diff --git a/.build/alpine.yaml b/.build/alpine.yaml deleted file mode 100644 index b5f8e32fa..000000000 --- a/.build/alpine.yaml +++ /dev/null @@ -1,59 +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: - - run - - '--privileged' - - 'linuxkit/binfmt:v0.7' - id: 'initialize-qemu' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - create - - '--name' - - multiarch-builder - id: 'create-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - use - - multiarch-builder - id: 'select-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - inspect - - '--bootstrap' - id: 'show-target-build-platforms' -- name: 'gcr.io/cloud-builders/docker' - args: - - 'buildx' - - 'build' - - '--platform' - - $_DOCKER_BUILDX_PLATFORMS - - '--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' - - '--push' - - '.' - id: 'build-multi-architecture-container-image' -options: - env: - - DOCKER_CLI_EXPERIMENTAL=enabled -substitutions: - _DOCKER_BUILDX_PLATFORMS: 'linux/amd64,linux/arm64' diff --git a/.build/buster.yaml b/.build/buster.yaml deleted file mode 100644 index 7d76ff992..000000000 --- a/.build/buster.yaml +++ /dev/null @@ -1,59 +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: - - run - - '--privileged' - - 'linuxkit/binfmt:v0.7' - id: 'initialize-qemu' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - create - - '--name' - - multiarch-builder - id: 'create-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - use - - multiarch-builder - id: 'select-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - inspect - - '--bootstrap' - id: 'show-target-build-platforms' -- name: 'gcr.io/cloud-builders/docker' - args: - - 'buildx' - - 'build' - - '--platform' - - $_DOCKER_BUILDX_PLATFORMS - - '--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' - - '--push' - - '.' - id: 'build-multi-architecture-container-image' -options: - env: - - DOCKER_CLI_EXPERIMENTAL=enabled -substitutions: - _DOCKER_BUILDX_PLATFORMS: 'linux/amd64,linux/arm64' diff --git a/.build/default.yaml b/.build/default.yaml deleted file mode 100644 index 913de26d7..000000000 --- a/.build/default.yaml +++ /dev/null @@ -1,58 +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: - - run - - '--privileged' - - 'linuxkit/binfmt:v0.7' - id: 'initialize-qemu' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - create - - '--name' - - multiarch-builder - id: 'create-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - use - - multiarch-builder - id: 'select-builder' -- name: gcr.io/cloud-builders/docker - args: - - buildx - - inspect - - '--bootstrap' - id: 'show-target-build-platforms' -- name: 'gcr.io/cloud-builders/docker' - args: - - 'buildx' - - 'build' - - '--platform' - - $_DOCKER_BUILDX_PLATFORMS - - '--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}' - - '--push' - - '.' - id: 'build-multi-architecture-container-image' -options: - env: - - DOCKER_CLI_EXPERIMENTAL=enabled -substitutions: - _DOCKER_BUILDX_PLATFORMS: 'linux/amd64,linux/arm64' diff --git a/.build/gcs_upload.yaml b/.build/gcs_upload.yaml deleted file mode 100644 index d99e047d2..000000000 --- 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/.build/release_artifacts.sh b/.build/release_artifacts.sh deleted file mode 100755 index 7748fc0d2..000000000 --- a/.build/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 cmd/cloud_sql_proxy/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/.build/tag_latest.sh b/.build/tag_latest.sh deleted file mode 100755 index afbc9596c..000000000 --- a/.build/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 --quiet "$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/.ci/cloudbuild.yaml b/.ci/cloudbuild.yaml new file mode 100644 index 000000000..2d94f7213 --- /dev/null +++ b/.ci/cloudbuild.yaml @@ -0,0 +1,114 @@ +# Copyright 2025 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: + + - id: run integration tests + name: golang:${_VERSION} + env: + [ + "IP_TYPE=${_IP_TYPE}", + "GOOGLE_CLOUD_PROJECT=${PROJECT_ID}", + "TMPDIR=/tmp" + ] + secretEnv: + [ + "MYSQL_CONNECTION_NAME", + "MYSQL_USER", + "MYSQL_PASS", + "MYSQL_DB", + "MYSQL_MCP_CONNECTION_NAME", + "MYSQL_MCP_PASS", + "POSTGRES_CONNECTION_NAME", + "POSTGRES_USER", + "POSTGRES_USER_IAM", + "POSTGRES_PASS", + "POSTGRES_DB", + "POSTGRES_CAS_CONNECTION_NAME", + "POSTGRES_CAS_PASS", + "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME", + "POSTGRES_CUSTOMER_CAS_PASS", + "POSTGRES_CUSTOMER_CAS_DOMAIN_NAME", + "POSTGRES_MCP_CONNECTION_NAME", + "POSTGRES_MCP_PASS", + "SQLSERVER_CONNECTION_NAME", + "SQLSERVER_USER", + "SQLSERVER_PASS", + "SQLSERVER_DB", + "IMPERSONATED_USER", + ] + entrypoint: bash + args: + - -c + - | + go test -race -v ./tests/... + +substitutions: + _VERSION: ${_VERSION} + _IP_TYPE: ${_IP_TYPE} + +availableSecrets: + secretManager: + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_CONNECTION_NAME/versions/latest" + env: "MYSQL_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_USER/versions/latest" + env: "MYSQL_USER" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_PASS/versions/latest" + env: "MYSQL_PASS" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_DB/versions/latest" + env: "MYSQL_DB" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_MCP_CONNECTION_NAME/versions/latest" + env: "MYSQL_MCP_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/MYSQL_MCP_PASS/versions/latest" + env: "MYSQL_MCP_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_USER/versions/latest" + env: "POSTGRES_USER" + - versionName: "projects/$PROJECT_ID/secrets/CLOUD_BUILD_POSTGRES_IAM_USER/versions/latest" + env: "POSTGRES_USER_IAM" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_PASS/versions/latest" + env: "POSTGRES_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_DB/versions/latest" + env: "POSTGRES_DB" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CAS_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CAS_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CAS_PASS/versions/latest" + env: "POSTGRES_CAS_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_PASS" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_DOMAIN_NAME/versions/latest" + env: "POSTGRES_CUSTOMER_CAS_DOMAIN_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_MCP_CONNECTION_NAME/versions/latest" + env: "POSTGRES_MCP_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/POSTGRES_MCP_PASS/versions/latest" + env: "POSTGRES_MCP_PASS" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_CONNECTION_NAME/versions/latest" + env: "SQLSERVER_CONNECTION_NAME" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_USER/versions/latest" + env: "SQLSERVER_USER" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_PASS/versions/latest" + env: "SQLSERVER_PASS" + - versionName: "projects/$PROJECT_ID/secrets/SQLSERVER_DB/versions/latest" + env: "SQLSERVER_DB" + - versionName: "projects/$PROJECT_ID/secrets/CLOUD_BUILD_SA/versions/latest" + env: "IMPERSONATED_USER" + +options: + dynamicSubstitutions: true + pool: + name: ${_POOL_NAME} + logging: CLOUD_LOGGING_ONLY diff --git a/.envrc.example b/.envrc.example index 889725bfc..6c31e2d55 100644 --- a/.envrc.example +++ b/.envrc.example @@ -15,3 +15,9 @@ 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 + +# Requires the impersonating IAM principal to have +# roles/iam.serviceAccountTokenCreator +export IMPERSONATED_USER="some-user-with-db-access@example.com" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d2262b409..7928cbece 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @GoogleCloudPlatform/infra-db-dpes +* @GoogleCloudPlatform/cloud-sql-connectors diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index a555a7142..000000000 --- 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/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..353a189b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,78 @@ +# Copyright 2023 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. + +name: 🐞 Bug Report +description: File a bug report +title: "Brief summary of what bug or error was observed" +labels: ["type: bug"] +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + Please run down the following list and make sure you've tried the usual "quick fixes": + - Search the [current open issues](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues) + - Check for answers on [StackOverflow](https://stackoverflow.com/questions/tagged/google-cloud-sql) (under the 'google-cloud-sql' tag) + + If you are still having issues, please include as much information as possible below! :smile: + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: "Please enter a detailed description of the bug, and any information about what behavior you noticed and why it is defective or unintentional." + validations: + required: true + - type: textarea + id: example-code + attributes: + label: Example code (or command) + description: "Please paste any useful application code related to the bug below. (if your code is in a public repo, feel free to paste a link!)" + value: | + ``` + // paste your code or command here + ``` + - type: textarea + id: stacktrace + attributes: + label: Stacktrace + description: "Paste any relevant stacktrace or error you are running into here. Be sure to filter sensitive information!" + render: bash + - type: textarea + id: repro + attributes: + label: Steps to reproduce? + description: "How do you trigger this bug? Please walk us through it step by step." + value: | + 1. ? + 2. ? + 3. ? + ... + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: "Let us know some details about the environment in which you are seeing the bug!" + value: | + 1. OS type and version: + 2. Cloud SQL Proxy version (`./cloud-sql-proxy --version`): + 3. Proxy invocation command (for example, `./cloud-sql-proxy --port 5432 INSTANCE_CONNECTION_NAME`): + validations: + required: true + - type: textarea + id: additional-details + attributes: + label: Additional Details + description: "Any other information you want us to know?" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 94e6589ed..000000000 --- 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 3692027d2..000000000 --- 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/documentation_issue.yaml b/.github/ISSUE_TEMPLATE/documentation_issue.yaml new file mode 100644 index 000000000..5c3cf5098 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_issue.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 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. + +name: 📝 Documentation Issue +description: Report wrong or missing information with the documentation in this repo. +title: "Brief summary of what is missing or incorrect" +labels: ["type: docs"] +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! :smile: + + Please explain below how we can improve our documentation. + - type: textarea + id: description + attributes: + label: Description + description: "Provide a short description of what is missing or incorrect, as well as a link to the specific location of the issue." + validations: + required: true + - type: textarea + id: potential-solution + attributes: + label: Potential Solution + description: "What would you prefer the documentation say? Why would this information be more accurate or helpful?" + - type: textarea + id: additional-details + attributes: + label: Additional Details + description: "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 6849df425..000000000 --- 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/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..c00e4acbf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,54 @@ +# Copyright 2023 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. + +name: ✨ Feature Request +description: Suggest an idea for new or improved behavior. +title: "Brief summary of the proposed feature" +labels: ["type: feature request"] +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + + Please run down the following list before proceeding with your feature request: + - Search the [current open issues](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues) to prevent creating a duplicate. + + Please include as much information as possible below! :smile: + - type: textarea + id: feature-description + attributes: + label: Feature Description + description: "A clear and concise description of what feature you would like to see, and why it would be useful to have added." + validations: + required: true + - type: textarea + id: sample-code + attributes: + label: Sample code + description: "If you already have an idea of what the implementation of this feature would like in code please provide it. (pseudo code is okay!)" + value: | + ``` + // sample code here + ``` + - type: textarea + id: alternatives-considered + attributes: + label: Alternatives Considered + description: "Are there any workaround or third party tools to replicate this behavior? Why would adding this feature be preferred over them?" + - type: textarea + id: additional-details + attributes: + label: Additional Details + description: "Any additional information we should know? Please reference it here (issues, PRs, descriptions, or screenshots)" diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 14f5e6bac..000000000 --- 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/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 000000000..88bf09f0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,48 @@ +# Copyright 2023 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. + +name: 💬 Question +description: Questions on how something works or the best way to do something? +title: "Brief summary of your question" +labels: ["type: question"] +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better!
+ + Please run down the following list and make sure you've tried the usual "quick fixes": + - Search the [current open issues](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues) for a similar question + - Check for answers on [StackOverflow](https://stackoverflow.com/questions/tagged/google-cloud-sql) (under the 'google-cloud-sql' tag) + + If you still have a question, please include as much information as possible below! :smile: + - type: textarea + id: question + attributes: + label: Question + description: "What's your question? Please provide as much relevant information as possible to reduce turnaround time." + placeholder: "Example: How do I connect using this connector with Private IP from Cloud Run?" + validations: + required: true + - type: textarea + id: code + attributes: + label: Code + description: "Please paste any useful application code that might be relevant to your question. (if your code is in a public repo, feel free to paste a link!)" + render: Go + - type: textarea + id: additional-details + attributes: + label: Additional Details + description: "Any other information you want us to know that might be helpful in answering your question? (link issues, PRs, descriptions, or screenshots)" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 9208f5ba3..000000000 --- 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 index de680bfe1..3867ea389 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -13,13 +13,9 @@ # limitations under the License. assign_issues: -# - shubha-rajan - - enocom -# - jackwotherspoon -# - kurtisvg + - hessjcg + - kgala2 assign_prs: -# - shubha-rajan - - enocom -# - jackwotherspoon -# - kurtisvg + - hessjcg + - kgala2 diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000..bec321a45 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,95 @@ +# 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: duplicate + color: ededed + description: "" + +- name: 'type: bug' + color: db4437 + description: Error or flaw in code with unintended results or allowing sub-optimal + usage patterns. +- name: 'type: cleanup' + color: c5def5 + description: An internal cleanup or hygiene concern. +- name: 'type: docs' + color: 0000A0 + description: Improvement to the documentation for an API. +- name: 'type: feature request' + color: c5def5 + description: ‘Nice-to-have’ improvement, new feature or different behavior or design. +- name: 'type: process' + color: c5def5 + description: A process-related concern. May include testing, release, or the like. +- name: 'type: question' + color: c5def5 + description: Request for information or clarification. + +- name: 'priority: p0' + color: b60205 + description: Highest priority. Critical issue. P0 implies highest priority. +- name: 'priority: p1' + color: ffa03e + description: Important issue which blocks shipping the next release. Will be fixed + prior to next release. +- name: 'priority: p2' + color: fef2c0 + description: Moderately-important priority. Fix may not be included in next release. +- name: 'priority: p3' + color: ffffc7 + description: Desirable enhancement or fix. May not be included in next release. + +- name: 'v1' + color: d93f0b + description: Functionality for v1 of the proxy. + +- name: automerge + color: 00ff00 + description: Merge the pull request once unit tests and other checks pass. +- name: 'automerge: exact' + color: 8dd517 + description: Summon MOG for automerging, but approvals need to be against the latest + commit +- name: do not merge + color: d93f0b + description: Indicates a pull request not ready for merge, due to either quality + or timing. + +- name: 'autorelease: pending' + color: ededed + description: Release please needs to do its work on this. +- name: 'autorelease: triggered' + color: ededed + description: Release please has triggered a release for this. +- name: 'autorelease: tagged' + color: ededed + description: Release please has completed a release for this. + +- name: 'tests: run' + color: 3DED97 + description: Label to trigger Github Action tests. + +- name: 'flakybot: flaky' + color: 86d9d7 + description: Tells the Flaky Bot not to close or comment on this issue. +- name: 'flakybot: quiet' + color: 86d9d7 + description: Tells the Flaky Bot to comment less. +- name: 'flakybot: issue' + color: a9f9f7 + description: An issue filed by the Flaky Bot. Should not be added manually. + +- name: 'k8s' + color: 1D76DB + description: Kubernetes related issue or functionality. diff --git a/.github/release-please.yml b/.github/release-please.yml index 24290d576..d6896751a 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -15,4 +15,17 @@ handleGHRelease: true packageName: cloud-sql-proxy releaseType: simple -versionFile: 'cmd/cloud_sql_proxy/version.txt' +versionFile: "cmd/version.txt" +branches: + - branch: v1 + versionFile: "proxy/util/version.txt" + handleGHRelease: true + packageName: cloud-sql-proxy + releaseType: simple +extraFiles: + - README.md + - cmd/root.go + - docs/cmd/cloud-sql-proxy.md + - examples/k8s-health-check/README.md + - examples/k8s-service/README.md + - examples/k8s-sidecar/README.md diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index d6c35b4ba..000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": [ - "config:base", - ":semanticCommitTypeAll(chore)" - ], - "ignorePresets": [":semanticPrefixFixDepsChoreOthers"], - "prConcurrentLimit": 0, - "rebaseStalePrs": true, - "dependencyDashboard": true, - "semanticCommits": true, - "postUpdateOptions": [ - "gomodTidy" - ], - "ignoreDeps": [ - "golang.org/x/net" - ], - "force": { - "constraints": { - "go": "1.16" - } - } -} diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..691e7a93b --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,26 @@ +{ + "extends": [ + "config:recommended" + ], + "dependencyDashboardLabels": ["type: process"], + "commitMessagePrefix": "deps: ", + "postUpdateOptions": [ + "gomodTidy" + ], + "prConcurrentLimit": 3, + "packageRules": [ + { + "matchManagers": ["github-actions"], + "groupName": "dependencies for github" + }, + { + "matchManagers": ["dockerfile"], + "groupName": "container images" + }, + { + "groupName": "Non-major dependency updates", + "matchManagers": ["gomod"], + "matchUpdateTypes": ["minor", "patch"], + }, + ] +} diff --git a/.github/trusted-contribution.yml b/.github/trusted-contribution.yml index 1a2f61205..244cb6d15 100644 --- a/.github/trusted-contribution.yml +++ b/.github/trusted-contribution.yml @@ -1,10 +1,10 @@ -# Copyright 2022 Google LLC +# Copyright 2025 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 +# 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, @@ -12,6 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Trigger presubmit tests for trusted contributors +# https://github.com/googleapis/repo-automation-bots/tree/main/packages/trusted-contribution +# Install: https://github.com/apps/trusted-contributions-gcf + +trustedContributors: + - "dependabot[bot]" + - "renovate-bot" + - "renovate[bot]" + - "forking-renovate[bot]" + - "release-please[bot]" annotations: - - type: label - text: "tests: run" + # Trigger Cloud Build tests + - type: comment + text: "/gcbrun" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..6104157e5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +# Copyright 2023 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: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + paths-ignore: + - "**/*.md" + - "**/*.txt" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["go"] + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.25" + if: ${{ matrix.language == 'go' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@4bdb89f48054571735e3792627da6195c57459e2 # v3.31.10 + with: + languages: ${{ matrix.language }} + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually + - name: Autobuild + uses: github/codeql-action/autobuild@4bdb89f48054571735e3792627da6195c57459e2 # v3.31.10 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@4bdb89f48054571735e3792627da6195c57459e2 # v3.31.10 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index f2528bce6..900892f72 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -15,17 +15,20 @@ name: code coverage on: [pull_request] +# Declare default permissions as read only. +permissions: read-all + jobs: build: runs-on: ubuntu-latest steps: - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: - go-version: "1.17" + go-version: "1.25" - name: Checkout base branch - uses: actions/checkout@v3 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.base_ref }} - name: Calculate base code coverage @@ -35,7 +38,7 @@ jobs: echo "CUR_COVER=$CUR_COVER" >> $GITHUB_ENV - name: Checkout PR branch - uses: actions/checkout@v3 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Calculate PR code coverage run: | go test -short -coverprofile pr_cover.out ./... || true @@ -45,6 +48,6 @@ jobs: - 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 + if [ "${PR_COVER}" -lt "${CUR_COVER}" ]; then exit 1; fi diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 000000000..5ff8c2154 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,37 @@ +# Copyright 2024 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: docs +on: + pull_request: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + docs: + name: Check docs are up to date + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.25" + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Generate docs and fail if there are differences + run: | + go install ./cmd/gendocs + gendocs + git diff --exit-code diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml new file mode 100644 index 000000000..408c2fb6e --- /dev/null +++ b/.github/workflows/govulncheck.yaml @@ -0,0 +1,51 @@ +# Copyright 2023 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: govulncheck + +# Declare default permissions as read only. +permissions: read-all + +on: + push: + branches: + - "main" + - "v1" + pull_request: + branches: + - "main" + - "v1" + schedule: + - cron: "0 2 * * *" + +jobs: + govulncheck_job: + runs-on: ubuntu-latest + name: Run govulncheck + steps: + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.25" + check-latest: true + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - id: govulncheck + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1 + with: + # Let actions/checkout above check-out the correct SHA + repo-checkout: false diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml new file mode 100644 index 000000000..d6f875f20 --- /dev/null +++ b/.github/workflows/labels.yaml @@ -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. + +name: Sync labels +on: + push: + branches: + - main + +# Declare default permissions as read only. +permissions: read-all + +jobs: + build: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7af2c0a28..4500f38b4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -13,45 +13,23 @@ # limitations under the License. name: lint -on: +on: pull_request: - pull_request_target: - types: [labeled] + +# Declare default permissions as read only. +permissions: read-all jobs: - lint: - if: "${{ github.event.action != 'labeled' || github.event.label.name == 'tests: run' }}" + lint: name: run lint runs-on: ubuntu-latest steps: - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: '1.16' - - name: Install goimports - run: go get golang.org/x/tools/cmd/goimports - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - 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 - - name: Remove PR Label - if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - try { - await github.rest.issues.removeLabel({ - name: 'tests: run', - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number - }); - } catch (e) { - console.log('Failed to remove label. Another job may have already removed it!'); - } + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.25" + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: lint + run: | + ./build.sh lint_ci diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..ca8741da5 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,70 @@ +# Copyright 2023 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: OSSF Scorecard +on: + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + schedule: + # weekly on Sunday + - cron: '0 20 * * 0' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + + steps: + - name: "Checkout code" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + + - name: Filter SARIF to skip false positives + # filter out DangerousWorkflow alerts as they do not account for safe use of labels to trigger actions + env: + SCORECARD_SKIPPED_RULE_IDS: "DangerousWorkflowID" + run: | + SCORECARD_SKIPPED_RULE_IDS_JSON=$(echo $SCORECARD_SKIPPED_RULE_IDS | jq -cR 'split(",")') + # Trim the SARIF file to remove false positive detections + cat results.sarif | jq '.runs[].results |= map(select(.ruleId as $id | '$SCORECARD_SKIPPED_RULE_IDS_JSON' | all($id != .)))' > resultsFiltered.sarif + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@4bdb89f48054571735e3792627da6195c57459e2 # v3.31.10 + with: + sarif_file: resultsFiltered.sarif diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0964d9754..4d82abf2c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,145 +16,97 @@ name: tests on: push: branches: - - 'main' - - 'v2' + - "main" + - "v1" pull_request: - pull_request_target: - types: [labeled] schedule: - - cron: '0 2 * * *' + - cron: "0 2 * * *" + +# Declare default permissions as read only. +permissions: read-all jobs: - unit: - # run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label) - if: "${{ (github.event.action != 'labeled' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) || github.event.label.name == 'tests: run' }}" - name: unit tests - runs-on: "ubuntu-latest" - strategy: - matrix: - go-version: [1.16, 1.17] - fail-fast: false - permissions: - contents: 'read' - id-token: 'write' + compilation: + name: FreeBSD and OpenBSD compilation check + runs-on: ubuntu-latest steps: - - name: Remove PR label - if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - try { - await github.rest.issues.removeLabel({ - name: 'tests: run', - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number - }); - } catch (e) { - console.log('Failed to remove label. Another job may have already removed it!'); - } - - name: Checkout code - uses: 'actions/checkout@v3' - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.go-version }} - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0.8.0' + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: - workload_identity_provider: ${{ secrets.PROVIDER_NAME }} - service_account: ${{ secrets.SERVICE_ACCOUNT }} - access_token_lifetime: 600s - - - name: Run tests - run: | - go install github.com/jstemmer/go-junit-report/v2@latest - go test -short -race -v ./... | tee test_results.txt - go-junit-report -in test_results.txt -set-exit-code -out unit_sponge_log.xml + go-version: "1.25" - - name: FlakyBot - if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && always() }} + - name: Verify FreeBSD and OpenBSD Builds run: | - curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L - chmod +x ./flakybot - ./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} + CGO_ENABLED=0 GOOS=freebsd go build + CGO_ENABLED=0 GOOS=openbsd go build integration: - # run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label) - if: "${{ (github.event.action != 'labeled' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) || github.event.label.name == 'tests: run' }}" name: integration tests + # run integration tests on all builds except pull requests from forks or dependabot + if: | + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] fail-fast: false permissions: - contents: 'read' - id-token: 'write' + contents: read + id-token: write + issues: write + pull-requests: write steps: - - name: Remove PR label - if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - try { - await github.rest.issues.removeLabel({ - name: 'tests: run', - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number - }); - } catch (e) { - console.log('Failed to remove label. Another job may have already removed it!'); - } - - name: Checkout code - uses: 'actions/checkout@v3' - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: - go-version: 1.18 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0.8.0' + go-version: "1.25" + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13 with: - workload_identity_provider: ${{ secrets.PROVIDER_NAME }} - service_account: ${{ secrets.SERVICE_ACCOUNT }} + workload_identity_provider: ${{ vars.PROVIDER_NAME }} + service_account: ${{ vars.SERVICE_ACCOUNT }} access_token_lifetime: 600s - - id: 'secrets' + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2.2.1 + + - id: secrets name: Get secrets - uses: 'google-github-actions/get-secretmanager-secrets@v0.5.0' + uses: google-github-actions/get-secretmanager-secrets@2b5f97c5a4b9c105e64646762ad4fc3f5128e6f5 # v2.2.5 with: secrets: |- - MYSQL_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME - MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER - MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS - MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB - POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME - POSTGRES_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER - POSTGRES_USER_IAM:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM - POSTGRES_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_PASS - POSTGRES_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB - SQLSERVER_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME - SQLSERVER_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER - SQLSERVER_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS - SQLSERVER_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_DB - + MYSQL_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME + MYSQL_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER + MYSQL_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS + MYSQL_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB + MYSQL_MCP_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_MCP_CONNECTION_NAME + MYSQL_MCP_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_MCP_PASS + POSTGRES_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME + POSTGRES_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER + POSTGRES_USER_IAM:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM + POSTGRES_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_PASS + POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB + POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME + POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS + POSTGRES_CUSTOMER_CAS_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_DOMAIN_NAME + POSTGRES_MCP_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_MCP_CONNECTION_NAME + POSTGRES_MCP_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_MCP_PASS + SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME + SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER + SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS + SQLSERVER_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_DB + IMPERSONATED_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/IMPERSONATED_USER + - name: Enable fuse config (Linux) if: runner.os == 'Linux' run: | @@ -162,26 +114,43 @@ jobs: - name: Run tests env: - GOOGLE_CLOUD_PROJECT: '${{ secrets.GOOGLE_CLOUD_PROJECT }}' - MYSQL_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}' - MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}' - MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}' - MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}' - POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}' - POSTGRES_USER: '${{ steps.secrets.outputs.POSTGRES_USER }}' - POSTGRES_USER_IAM: '${{ steps.secrets.outputs.POSTGRES_USER_IAM }}' - POSTGRES_PASS: '${{ steps.secrets.outputs.POSTGRES_PASS }}' - POSTGRES_DB: '${{ steps.secrets.outputs.POSTGRES_DB }}' - SQLSERVER_CONNECTION_NAME: '${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}' - SQLSERVER_USER: '${{ steps.secrets.outputs.SQLSERVER_USER }}' - SQLSERVER_PASS: '${{ steps.secrets.outputs.SQLSERVER_PASS }}' - SQLSERVER_DB: '${{ steps.secrets.outputs.SQLSERVER_DB }}' + GOOGLE_CLOUD_PROJECT: "${{ vars.GOOGLE_CLOUD_PROJECT }}" + MYSQL_CONNECTION_NAME: "${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}" + MYSQL_USER: "${{ steps.secrets.outputs.MYSQL_USER }}" + MYSQL_PASS: "${{ steps.secrets.outputs.MYSQL_PASS }}" + MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}" + MYSQL_MCP_CONNECTION_NAME: "${{ steps.secrets.outputs.MYSQL_MCP_CONNECTION_NAME }}" + MYSQL_MCP_PASS: "${{ steps.secrets.outputs.MYSQL_MCP_PASS }}" + POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}" + POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}" + POSTGRES_USER_IAM: "${{ steps.secrets.outputs.POSTGRES_USER_IAM }}" + POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}" + POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}" + POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}" + POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}" + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}" + POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}" + POSTGRES_CUSTOMER_CAS_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME }}" + POSTGRES_MCP_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_MCP_CONNECTION_NAME }}" + POSTGRES_MCP_PASS: "${{ steps.secrets.outputs.POSTGRES_MCP_PASS }}" + SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}" + SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}" + SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}" + SQLSERVER_DB: "${{ steps.secrets.outputs.SQLSERVER_DB }}" + IMPERSONATED_USER: "${{ steps.secrets.outputs.IMPERSONATED_USER }}" TMPDIR: "/tmp" + TMP: "${{ runner.temp }}" + # specifying bash shell ensures a failure in a piped process isn't lost by using `set -eo pipefail` + shell: bash run: | - go install github.com/jstemmer/go-junit-report/v2@latest go test -race -v ./... | tee test_results.txt + + - name: Convert test output to XML + if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && always() }} + run: | + go install github.com/jstemmer/go-junit-report/v2@latest go-junit-report -in test_results.txt -set-exit-code -out integration_sponge_log.xml - + - name: FlakyBot (Linux) # only run flakybot on periodic (schedule) and continuous (push) events if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && runner.os == 'Linux' && always() }} @@ -189,7 +158,7 @@ jobs: curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L chmod +x ./flakybot ./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - + - name: FlakyBot (Windows) # only run flakybot on periodic (schedule) and continuous (push) events if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && runner.os == 'Windows' && always() }} @@ -204,3 +173,19 @@ jobs: curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot-darwin-amd64 -o flakybot -s -L chmod +x ./flakybot ./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} + + unit: + name: unit tests + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: 1.25 + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Run tests + # specifying bash shell ensures a failure in a piped process isn't lost by using `set -eo pipefail` + shell: bash + run: | + go test -v -race -short ./... diff --git a/.github/workflows/v2-periodic.yaml b/.github/workflows/v1-periodic.yaml similarity index 67% rename from .github/workflows/v2-periodic.yaml rename to .github/workflows/v1-periodic.yaml index 7c8ed5244..fb57fd294 100644 --- a/.github/workflows/v2-periodic.yaml +++ b/.github/workflows/v1-periodic.yaml @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: v2 periodic +name: v1 periodic on: schedule: - - cron: '0 2 * * *' + - cron: "0 2 * * *" + +# Declare default permissions as read only. +permissions: read-all jobs: integration: @@ -26,33 +29,30 @@ jobs: os: [macos-latest, windows-latest, ubuntu-latest] fail-fast: false permissions: - contents: 'read' - id-token: 'write' + contents: "read" + id-token: "write" steps: - name: Checkout code - uses: 'actions/checkout@v3' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: v2 + ref: v1 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: - go-version: 1.18 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0.8.0' + go-version: "1.25" + + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.13 with: workload_identity_provider: ${{ secrets.PROVIDER_NAME }} service_account: ${{ secrets.SERVICE_ACCOUNT }} access_token_lifetime: 600s - - name: 'Set up Cloud SDK' - uses: 'google-github-actions/setup-gcloud@v0.6.0' - - - id: 'secrets' + - id: secrets name: Get secrets - uses: 'google-github-actions/get-secretmanager-secrets@v0.5.0' + uses: google-github-actions/get-secretmanager-secrets@2b5f97c5a4b9c105e64646762ad4fc3f5128e6f5 # v2.2.5 with: secrets: |- MYSQL_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME @@ -68,7 +68,7 @@ jobs: SQLSERVER_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER SQLSERVER_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS SQLSERVER_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_DB - + - name: Enable fuse config (Linux) if: runner.os == 'Linux' run: | @@ -76,27 +76,33 @@ jobs: - name: Run tests env: - GOOGLE_CLOUD_PROJECT: '${{ secrets.GOOGLE_CLOUD_PROJECT }}' - MYSQL_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}' - MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}' - MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}' - MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}' - POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}' - POSTGRES_USER: '${{ steps.secrets.outputs.POSTGRES_USER }}' - POSTGRES_USER_IAM: '${{ steps.secrets.outputs.POSTGRES_USER_IAM }}' - POSTGRES_PASS: '${{ steps.secrets.outputs.POSTGRES_PASS }}' - POSTGRES_DB: '${{ steps.secrets.outputs.POSTGRES_DB }}' - SQLSERVER_CONNECTION_NAME: '${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}' - SQLSERVER_USER: '${{ steps.secrets.outputs.SQLSERVER_USER }}' - SQLSERVER_PASS: '${{ steps.secrets.outputs.SQLSERVER_PASS }}' - SQLSERVER_DB: '${{ steps.secrets.outputs.SQLSERVER_DB }}' + GOOGLE_CLOUD_PROJECT: "${{ secrets.GOOGLE_CLOUD_PROJECT }}" + MYSQL_CONNECTION_NAME: "${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}" + MYSQL_USER: "${{ steps.secrets.outputs.MYSQL_USER }}" + MYSQL_PASS: "${{ steps.secrets.outputs.MYSQL_PASS }}" + MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}" + POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}" + POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}" + POSTGRES_USER_IAM: "${{ steps.secrets.outputs.POSTGRES_USER_IAM }}" + POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}" + POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}" + SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}" + SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}" + SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}" + SQLSERVER_DB: "${{ steps.secrets.outputs.SQLSERVER_DB }}" TMPDIR: "/tmp" - TMP: '${{ runner.temp }}' + TMP: "${{ runner.temp }}" + # specifying bash shell ensures a failure in a piped process isn't lost by using `set -eo pipefail` + shell: bash run: | - go install github.com/jstemmer/go-junit-report/v2@latest go test -race -v ./... | tee test_results.txt - go-junit-report -in test_results.txt -set-exit-code -out v2periodic_sponge_log.xml - + + - name: Convert test output to XML + if: ${{ github.event_name == 'schedule' && always() }} + run: | + go install github.com/jstemmer/go-junit-report/v2@latest + go-junit-report -in test_results.txt -set-exit-code -out v1periodic_sponge_log.xml + - name: FlakyBot (Linux) # only run flakybot on periodic (schedule) event if: ${{ github.event_name == 'schedule' && runner.os == 'Linux' && always() }} @@ -104,7 +110,7 @@ jobs: curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L chmod +x ./flakybot ./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - + - name: FlakyBot (Windows) # only run flakybot on periodic (schedule) event if: ${{ github.event_name == 'schedule' && runner.os == 'Windows' && always() }} diff --git a/.gitignore b/.gitignore index 8bd39d275..de8224085 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,12 @@ # IDEs .idea/ .vscode/ +*.iml -# Compiled binary -/cmd/cloud_sql_proxy/cloud_sql_proxy -/cloud_sql_proxy +/cloud-sql-proxy +/cloud-sql-proxy.exe + +/key.json +/logs/ +.tools +test_results.txt diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..c6706b8e2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,67 @@ +# 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. +# .golangci.yml +version: "2" +linters: + default: none + enable: + - govet + - ineffassign + - revive + - staticcheck + - unused + settings: + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: empty-block + - name: errorf + - name: error-naming + - name: error-return + - name: error-strings + - name: exported + - name: if-return + - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + - name: range + - name: range-val-address + - name: range-val-in-closure + - name: receiver-naming + - name: redefines-builtin-id + - name: superfluous-else + - name: time-naming + - name: unexported-return + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: var-naming + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/CHANGELOG.md b/CHANGELOG.md index a726b479f..4f60ab9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,467 @@ # Changelog +## [2.21.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.21.0...v2.21.1) (2026-02-18) + + +### Bug Fixes + +* Correctly set the credentials token source and the WithIAMAuthN opt ([#2544](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2544)) ([5b14b6a](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/5b14b6af0226758b04555394b6bf08c15efbdcdf)), closes [#2542](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2542) +* Set the universe domain when using an impersonation chain. ([#2547](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2547)) ([db8644c](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/db8644c1ceeb598f89104892305b06d13c08d1a3)) +* Update deps Go Connector to version 1.20.1 ([#2555](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2555)) ([0a98362](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/0a98362f329215a6e1c7e35e422126ce7e0a5daa)) + +## [2.21.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.20.0...v2.21.0) (2026-01-16) + + +### Bug Fixes + +* Update deprecated proxy options and update all dependencies ([#2531](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2531)) ([d9ae096](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/d9ae0963767c7e70d446ebd08f1a96acef7a0342)) + + +### Miscellaneous Chores + +* release 2.21.0 ([#2533](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2533)) ([d813423](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/d813423df6eb938e9029c4f47a1bf9a459ac6a9f)) + +## [2.20.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.19.0...v2.20.0) (2025-12-09) + + +### Features + +* Add shutdown command to call the /quitquitquit endpoint. ([#2514](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2514)) ([f6747f9](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/f6747f93af92d235b449851599d1ef31642cfa11)), closes [#2511](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2511) + + +### Bug Fixes + +* Update dependency versions. ([#2520](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2520)) ([119dc23](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/119dc23efaf493411f4c92d0036eee7e843d8aba)) + +## [2.19.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.18.3...v2.19.0) (2025-10-29) + + +### Features + +* Add MySQL caching_sha2_password support for the proxy clients using unix sockets. ([#2489](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2489)) ([6ff8753](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/6ff8753f486e65afcd08ffea9b41be82c7d507f5)), closes [#2317](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2317) + +## [2.18.3](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.18.2...v2.18.3) (2025-10-23) + + +### Bug Fixes + +* bump dependencies to latest ([#2499](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2499)) ([c5fb6ee](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/c5fb6ee1079956c546d2da74c1fe2dfade3ac969)) + +## [2.18.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.18.1...v2.18.2) (2025-08-27) + + +### Bug Fixes + +* Update go connector dependency to 1.18.1 to include high priority fix for DNS name configurations. ([#2487](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2487)) ([91efc78](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/91efc78a63866df44b56707684650a86856a0dd9)) + +## [2.18.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.18.0...v2.18.1) (2025-08-14) + + +### Bug Fixes + +* bump dependencies to latest ([#2482](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2482)) ([1fc458c](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/1fc458c1e309442efa03cdb824c73f37505aa464)) + +## [2.18.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.17.1...v2.18.0) (2025-07-11) + + +### Features + +* add --skip-failed-instance-config flag, to ignore unix socket connection errors on startup ([#2452](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2452)) ([05b0d60](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/05b0d6067c178fdcd70267ccc2cd78614a292d5b)), closes [#2451](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2451) + +## [2.17.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.17.0...v2.17.1) (2025-06-11) + + +### Bug Fixes + +* Fix the release artifacts job. ([#2461](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2461)) ([929b09e](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/929b09e9be30b0995e105c0cc5f4448357f6490d)) + +## [2.17.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.16.0...v2.17.0) (2025-06-11) + + +### Bug Fixes + +* bump dependencies to latest ([#2455](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2455)) ([5035280](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/50352807af4a97797780894dd4fa16607f291582)) + + +### Miscellaneous Chores + +* release 2.17.0 ([#2459](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2459)) ([a78d853](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/a78d853b5ede5305e19a838ef7948c67d9f77083)) + +## [2.16.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.15.3...v2.16.0) (2025-04-28) + +### Features: + +* Update TLS validation to use both SAN and CN fields, from Go Connector v1.17.0 ([#979](https://github.com/GoogleCloudPlatform/cloud-sql-go-connector/issues/979)) ([df60a20](https://github.com/GoogleCloudPlatform/cloud-sql-go-connector/commit/df60a2061dbfd78ce30a87319be8d8e027957d86)) + + +## [2.15.3](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.15.2...v2.15.3) (2025-04-16) + + + +### Bug Fixes + +* bump dependencies to latest ([#2429](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2429)) ([0b1c5f4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/0b1c5f498f6e1bd8bbb7439030072b09fdb1aacb)) + +## [2.15.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.15.1...v2.15.2) (2025-03-20) + + +### Bug Fixes + +* bump dependencies to latest ([#2408](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2408)) ([43119f9](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/43119f9ba6f8c91cfb8d312647a8db55315e1ad4)) + +## [2.15.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.15.0...v2.15.1) (2025-02-19) + + +### Bug Fixes + +* bump dependencies to latest ([#2389](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2389)) ([852a16e](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/852a16eb2ab70c5f49c88873abc3e5cfee5875de)) + +## [2.15.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.14.3...v2.15.0) (2025-01-30) + + +### Features + +* enable Proxy to be started with DNS names ([#2363](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2363)) ([d902441](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/d902441dccdb17229c25478f7267a96917cd6a07)) + +## [2.14.3](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.14.2...v2.14.3) (2025-01-16) + + +### Bug Fixes + +* bump dependencies to latest ([#2367](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2367)) ([bab696f](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/bab696f59aaea4f4be47bb068432cabfc50d4777)) + +## [2.14.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.14.1...v2.14.2) (2024-12-11) + + +### Bug Fixes + +* bump dependencies to latest ([#2346](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2346)) ([a407a18](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/a407a1801fa1bf4b5afed581d6fe72ad1c45d16b)) + +## [2.14.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.14.0...v2.14.1) (2024-11-21) + + +### Bug Fixes + +* bump dependencies to latest ([#2328](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2328)) ([87975c4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/87975c4cae0877b48eaefedf544d5a80300f2a57)) + +## [2.14.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.13.0...v2.14.0) (2024-10-23) + + +### Features + +* callback notify function when connection is refused ([#2308](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2308)) ([9309b84](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/9309b8461d73d83137943885aad164793a14a875)) + +## [2.13.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.12.0...v2.13.0) (2024-08-14) + + +### Features + +* bump to Go 1.23 ([#2287](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2287)) ([fd6bd49](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/fd6bd49242c884508f641c754eb5cec5d28ac665)) + +## [2.12.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.11.4...v2.12.0) (2024-07-17) + + +### Features + +* Add parameter --min-sigterm-delay allow new connections for a minimum number off seconds before shutting down the proxy. ([#2266](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2266)) ([52cd0d9](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/52cd0d95695d2b8e9456825e7c0bd452234a867b)), closes [#1640](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1640) +* add support for Debian bookworm ([#2267](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2267)) ([fbec17b](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/fbec17bd2c8c0898bdf41eb22669a871e5048ba9)) + + +### Bug Fixes + +* ignore go-fuse ctx in Lookup ([#2268](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2268)) ([ae8ec35](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/ae8ec359b43056fe815ac7c649388232bc1b4171)) +* Make the process exit if there as an error accepting a fuse connection. ([#2257](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2257)) ([bb2a0ae](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/bb2a0ae76d518eaeec69fcc2ac7e930a4bd7e024)) + +## [2.11.4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.11.3...v2.11.4) (2024-06-12) + + +### Bug Fixes + +* bump dependencies to latest ([#2249](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2249)) ([6501df5](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/6501df5f34fdf82651bea163b9014ea15dc86b81)) + +## [2.11.3](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.11.2...v2.11.3) (2024-05-28) + + +### Bug Fixes + +* bump dependencies to latest ([#2236](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2236)) ([14ff947](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/14ff947fa6c3b9a0023d5be7ad5b165cf6ac153b)) + +## [2.11.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.11.1...v2.11.2) (2024-05-16) + + +### Bug Fixes + +* bump dependencies to latest ([#2218](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2218)) ([44dff63](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/44dff63c0d9ec755565ab54f1dd48e9967f6d513)) + +## [2.11.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.11.0...v2.11.1) (2024-05-14) + + +### Bug Fixes + +* don't depend on downstream in readiness check ([#2207](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2207)) ([49fa927](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/49fa927ede69bf24f3fd0c56e60b99e4111d58f1)), closes [#2083](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2083) +* ensure proxy shutsdown cleanly on fuse error ([#2205](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2205)) ([54e65d1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/54e65d14a5d533f44e33b52a2dc88c2a419eae2f)), closes [#2013](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2013) +* use public mirrors for base images ([#2190](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2190)) ([69b4215](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/69b42158291b0ea4f074469dabbe34949af86053)) + +## [2.11.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.10.1...v2.11.0) (2024-04-16) + + +### Features + +* add support for a lazy refresh ([#2184](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2184)) ([fd7ab82](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/fd7ab82796c052ddf12f78989e5d3cab49f26c55)), closes [#2183](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2183) +* use Google managed base images ([#2159](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2159)) ([1103a95](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/1103a95adb0c0751df99704f71a4376ce38613a4)) + +## [2.10.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.10.0...v2.10.1) (2024-03-20) + + +### Bug Fixes + +* correct CI/CD build error ([#2155](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2155)) ([3e3b8ed](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/3e3b8ede607a50ce72af8f8d1d86eb6789560aef)) + +## [2.10.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.9.0...v2.10.0) (2024-03-14) + + +### Features + +* add support for config file ([#2106](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2106)) ([c936396](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/c9363966cb017cde7712426c3e9c999e3d7e0973)) +* add TPC support ([#2116](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2116)) ([7d011f8](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/7d011f8f1bb87488f639a3bfde09f57ac350ab8c)) + + +### Bug Fixes + +* use kebab case for config file consistently ([#2130](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2130)) ([ee52f07](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/ee52f0759a84bad9d8cec4a3cd1f8ff536c2e982)) + +## [2.9.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.8.2...v2.9.0) (2024-02-20) + + +### Features + +* add support for debug logging ([#2107](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2107)) ([c8f7a0a](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/c8f7a0abc325a9183b23710e30f5d1c9e619aef5)) + +## [2.8.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.8.1...v2.8.2) (2024-01-17) + + +### Bug Fixes + +* update dependencies to latest versions ([#2089](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2089)) ([6d9981a](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/6d9981a757e3c1033954db7b6f834c42c5495e4f)) + +## [2.8.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.8.0...v2.8.1) (2023-12-12) + + +### Bug Fixes + +* label container images correctly ([#2061](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2061)) ([f071d38](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/f071d38e152c70113d7102c19ed450c74e8d64f0)) +* Update Go Connector to v1.5.2 to ensure connections work after waking from sleep +([#1788](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1788)) + +## [2.8.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.7.2...v2.8.0) (2023-12-04) + + +### Features + +* add support for wait command ([#2041](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2041)) ([1c00ba4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/1c00ba475374e6ae46956c4125b91a55fe953751)) + +## [2.7.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.7.1...v2.7.2) (2023-11-14) + + +### Bug Fixes + +* use proper error instance to write error log ([#2014](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2014)) ([cc76a54](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/cc76a544a6878ad9f0ef5fb407de314b3f801cbe)) + +## [2.7.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.7.0...v2.7.1) (2023-10-17) + + +### Bug Fixes + +* bump dependencies to latest ([#2004](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2004)) ([4953402](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/4953402d9a63613729a2f5b8a33ac0323b7b6bb9)) + +## [2.7.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.6.1...v2.7.0) (2023-09-19) + + +### Features + +* /quitquitquit api now responds to HTTP GET and POST requests. ([#1947](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1947)) ([e5ebb48](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/e5ebb485f7a7a5f9820822bf4e84467da431fc6b)), closes [#1946](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1946) +* Add support for systemd notify ([#1930](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1930)) ([cf23647](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/cf23647f72990fc3e6b4987e3040c6020929b97d)) + +## [2.6.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.6.0...v2.6.1) (2023-08-16) + + +### Bug Fixes + +* remove the error message for zero on sigterm ([#1902](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1902)) ([55f0f60](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/55f0f60c9701a22e657ba814c9cfe6221c4840e7)) + +## [2.6.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.5.0...v2.6.0) (2023-07-14) + + +### Features + +* add support for exit zero on sigterm ([#1870](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1870)) ([e0a97dd](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/e0a97ddd5bea94054b1da0c3f0844ab47ad6f126)) + +## [2.5.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.4.0...v2.5.0) (2023-07-11) + + +### Features + +* add PSC support to proxy ([#1863](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1863)) ([496974a](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/496974a31555d64f144d24247507c0ec457d7edd)) + +## [2.4.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.3.0...v2.4.0) (2023-06-14) + + +### Features + +* add connection test for startup ([#1832](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1832)) ([47dae85](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/47dae851a9513bb5e3d98b59a33aef909a2bf125)), closes [#348](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/348) +* allow connections during shutdown ([#1805](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1805)) ([4a456ed](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/4a456ed6a9727f672783aee021b20a208971270d)) + + +### Bug Fixes + +* log info message for quitquitquit ([#1806](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1806)) ([4d36204](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/4d36204cb6c93751e9a7d40be5e3eff94a90847f)) +* Windows service stop ([#1833](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1833)) ([17e66a7](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/17e66a7a73a88d5c29c77133cdb5ad5aebd0a4c1)) + +## [2.3.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.2.0...v2.3.0) (2023-05-16) + + +### Features + +* Add Windows Service support ([#1696](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1696)) ([ec56eba](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/ec56ebab683804885edd95365e099de7a0de578f)), closes [#277](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/277) + + +### Bug Fixes + +* disallow auto-iam-authn with gcloud-auth ([#1762](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1762)) ([8200abe](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/8200abe467bdf9f5b458f108e5f086bdfbfa2dd9)), closes [#1754](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1754) + +## [2.2.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.1.2...v2.2.0) (2023-04-18) + + +### Features + +* add support for Auto IP ([#1735](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1735)) ([83c8a64](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/83c8a6444e9e1305922550bd5b6ac373babb0ffc)) + + +### Bug Fixes + +* allow `--structured-logs` and `--quiet` flags together ([#1750](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1750)) ([0aff60e](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/0aff60e5daf7995890ebc750080032bed543c9ea)) +* limit calls to SQL Admin API on startup ([#1723](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1723)) ([e1a03df](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/e1a03df61120e26c7bffe86a1f971cca8bb77562)) +* pass dial options to FUSE mounts ([#1737](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1737)) ([7ecf6ac](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/7ecf6ac013760a1e775db4a8da6a99a1e1817330)) + +## [2.1.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.1.1...v2.1.2) (2023-03-22) + + +### Bug Fixes + +* update dependencies to latest versions ([#1707](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1707)) ([54ea90e](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/54ea90e140873da254a34ea8b4e612b81a46cf13)) + +## [2.1.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.1.0...v2.1.1) (2023-02-23) + + +### Bug Fixes + +* build statically linked binaries ([#1680](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1680)) ([49308c5](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/49308c5c3c2372e4cb7e26f58c1e0dba7953f663)) + +## [2.1.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0...v2.1.0) (2023-02-16) + + +### Features + +* add support for Go 1.20 ([#1630](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1630)) ([72df17d](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/72df17d9a1992d51faf3a9f4ecd3960f680b7ef3)) +* add support for quitquitquit endpoint ([#1624](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1624)) ([43f9857](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/43f98574de06211581779a67806b01d5518cdd62)) +* Add unix-socket-path to instance command arguments. ([#1623](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1623)) ([f42f3d1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/f42f3d1ce9fc81b78d9e8bd68b147cae20516fae)), closes [#1573](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1573) + + +### Bug Fixes + +* ensure separate token source with auto-iam-authn ([#1637](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1637)) ([325a487](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/325a487187c9a9cb1864b8f387b1b06369e1ca25)) +* honor request context in readiness check ([#1657](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1657)) ([0934739](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/09347395eddd8b4942a1cfb77344b014c8bdc90b)) + +## [2.0.0](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0-preview.4...v2.0.0) (2023-01-17) + + +### Bug Fixes + +* correctly apply metadata to user agent ([#1606](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1606)) ([1ca9902](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/1ca9902fb949ea2a75fcdc5ed9930877db6ff600)) + + +### Miscellaneous Chores + +* release 2.0.0 ([#1615](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1615)) ([4a6283b](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/4a6283b70b49f97a5b60ebb68c9e01d6add2dff0)) + +## [2.0.0-preview.4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0-preview.3...v2.0.0-preview.4) (2022-12-12) + + +### Features + +* add admin server with pprof ([#1564](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1564)) ([d022c56](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/d022c5683a301722e55692ae3ca1d62cf0e6d017)) + + +### Bug Fixes + +* add runtime version to user agent if present ([#1542](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1542)) ([a6b689b](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/a6b689b05aa6f1e11ede8a1dd6fdec3cfc3c8c8e)) +* use user-agent as flag name ([#1561](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1561)) ([e1b2f7e](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/e1b2f7eb4e9552a1124b7aab3a1bca4366797b53)) + + +### Miscellaneous Chores + +* release 2.0.0-preview.4 ([#1576](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1576)) ([04fcf88](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/04fcf88d35a1da7e623b763829a02e79431fb74e)) + +## [2.0.0-preview.3](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0-preview.2...v2.0.0-preview.3) (2022-11-15) + + +### Features + +* add quiet flag ([#1515](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1515)) ([93d9a40](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/93d9a40cf8736bfce2d3cc6bc20b2defafa0413f)), closes [#1452](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1452) +* add support for min ready instances ([#1496](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1496)) ([73e2999](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/73e2999f3f3da32a63149a2d0cc08750038f721f)) +* configure the proxy with environment variables ([#1514](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1514)) ([2a9d9a2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/2a9d9a2cd93804818a5659ee554c56336969d861)) + + +### Bug Fixes + +* correct bullseye Dockerfile ([#1504](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1504)) ([15a97e7](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/15a97e7f8287098232719cfae2a2ad6242a7a92a)) +* correct error check in check connections ([#1505](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1505)) ([776a86b](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/776a86b15f14d1c0846f9813845829ffb9642bb8)) +* impersonated user uses downscoped token ([#1520](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1520)) ([b08c71d](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/b08c71d02ea7c33c587a0a75c30e06242029028b)) +* return correct exit code on SIGTERM ([#1530](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1530)) ([7bb15aa](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/7bb15aa482c2278f6e49a3a0f4a7baf4a2f4b511)) + + +### Miscellaneous Chores + +* release 2.0.0-preview.3 ([#1548](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1548)) ([024963b](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/024963b9b8b38fbb8b2d7043be5a5658a77689c0)) + +## [2.0.0-preview.2](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0-preview.1...v2.0.0-preview.2) (2022-10-25) + + +### Features + +* add bullseye container image ([#1468](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1468)) ([36a0172](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/36a01725b6d1ef30450570d3780871521a3ed6f3)) +* add support for impersonation ([#1460](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1460)) ([d0f8e55](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/d0f8e55ccb390b9c1f803b3a6c4f2e7874f40337)), closes [#417](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/417) +* add support for JSON credentials flag ([#1433](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1433)) ([2a9c8d8](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/2a9c8d8cb24e2c84e43620dab333677191d1dbd7)) +* bump to Go 1.19 ([#1411](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1411)) ([02e008a](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/02e008a6d886a461a76ddc899c8891621ca2f58c)) +connector/commit/bd20b6bfe746cfea778b9e1a9702de28047e5950)) +* cloud.google.com/go/cloudsqlconn: Downscope OAuth2 token included in ephemeral certificate ([#​332](https://togithub.com/GoogleCloudPlatform/cloud-sql-go-connector/issues/332)) ([d13dd6f](https://togithub.com/GoogleCloudPlatform/cloud-sql-go-connector/commit/d13dd6f3e7db0179511539315dec1c2dc96f0e3e)) + + +### Bug Fixes + +* don't build FUSE paths for Windows ([#1400](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1400)) ([be2d14f](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/be2d14f39fa88f17cf69cf338719f08d2f81143b)) +* restore openbsd and freebsd support ([#1442](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1442)) ([05dcdd4](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/05dcdd4b48bc6fabba14ae41dcc5de7c1d0c3f2f)) +* set write permissions for group and other ([#1405](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1405)) ([f6b77d7](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/f6b77d7b42633f689be877e469173fa42a6877a8)) +* cloud.google.com/go/cloudsqlconn: throw error when Auto IAM AuthN is unsupported ([#​310](https://togithub.com/GoogleCloudPlatform/cloud-sql-go-connector/issues/310)) ([652e196](https://togithub.com/GoogleCloudPlatform/cloud-sql-go-connector/commit/652e196b427ce9673676e214c6ad3905b21a68b0)) + + +### Miscellaneous Chores + +* release 2.0.0-preview.2 ([#1503](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1503)) ([67345e9](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/67345e9b7986d0951f73ecdfefb5f0d7ef2eef18)) + +## [2.0.0-preview.1](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/compare/v2.0.0-preview.0...v2.0.0-preview.1) (2022-09-07) + + +### Features + +* add support for FUSE ([#1381](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1381)) ([6cf4d25](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/6cf4d255fe7d640db4e7e651aed9c377ecf4e735)) + + +### Bug Fixes + +* pass dial options when checking connections ([#1366](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1366)) ([0033c36](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/0033c36200e7b5ba77b8d1157b1168af5fba73fc)) +* support configuration of HTTP server address ([#1365](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/1365)) ([b53d77f](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/commit/b53d77fce751af9316a4bc1349cd3bbcaaf151b0)) + ## [1.31.2](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.31.1...v1.31.2) (2022-08-02) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..46b2a08ea --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, +and in the interest of fostering an open and welcoming community, +we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project +a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, +such as physical or electronic +addresses, without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct. +By adopting this Code of Conduct, +project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. +Project maintainers who do not follow or enforce the Code of Conduct +may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported by opening an issue +or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, +available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48cf8e8fc..37ae0a447 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,193 +1,33 @@ -# Contributing +# How to Contribute -1. **Please sign one of the contributor license agreements below!** -1. Fork the repo, develop and test your code changes, add docs. -1. Make sure that your commit messages clearly describe the changes. -1. Send a pull request. +We'd love to accept your patches and contributions to this project. -## Table of contents -* [Opening an issue](#opening-an-issue) -* [How to run tests](#how-to-run-tests) -* [Contributor License Agreements](#contributor-license-agreements) -* [Contributor Code of Conduct](#contributor-code-of-conduct) +## Before you begin -## Opening an issue +### Sign our Contributor License Agreement -If you find a bug in the proxy code or an inaccuracy in the documentation, -please open an issue. GitHub provides a guide, [Mastering -Issues](https://guides.github.com/features/issues/), that is useful if you are -unfamiliar with the process. Here are the specific steps for opening an issue: +Contributions to this project must be accompanied by a +[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). +You (or your employer) retain the copyright to your contribution; this simply +gives us permission to use and redistribute your contributions as part of the +project. -1. Go to the project issues page on GitHub. -1. Click the green `New Issue` button located in the upper right corner. -1. In the title field, write a single phrase that identifies your issue. -1. In the main editor, describe your issue. -1. Click the submit button. +If you or your current employer have already signed the Google CLA (even if it +was for a different project), you probably don't need to do it again. -Thank you. We will do our best to triage your issue within one business day, and -attempt to categorize your issues with an estimate of the priority and issue -type. We will try to respond with regular updates based on its priority: +Visit to see your current agreements or to +sign a new one. -* **Critical** respond and update daily, resolve with a week -* **High** respond and update weekly, resolve within six weeks -* **Medium** respond and update every three months, best effort resolution -* **Low** respond and update every six months, best effort resolution +### Review our Community Guidelines -The priority we assign will be roughly a function of the number of users we -expect to be impacted, as well as its severity. As a rule of thumb: +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). - - - - - - - - - - - - - - - - - - - - - - - - - - +## Contribution process - - - - - - - -
SeverityNumber of users
HandfulSomeMostAll
Easy, obvious workaroundLowLowMediumHigh -
Non-obvious workaround availableLowMediumHighCritical
Functionality blockedHighHighCriticalCritical
+### Code Reviews -## How to run tests - -The test suite includes both unit and integration tests. For macOS and Linux, -there is a depenency on [FUSE][] that must be present on the system. - -### Test Dependencies - -When running tests on macOS and Linux, users will need to first install -[FUSE][]. Windows users may skip this step. - -On Debian, use: - -``` -sudo apt-get install fuse -``` - -On macOS, use: - -``` -brew install --cask macfuse -``` - -### How to run just unit tests - -``` -go test -short ./... -``` - -### How to run all tests - -To run all integration tests, users will need a Google Cloud project with a -MySQL, PostgreSQL, and SQL Server database, in addition to installing FUSE -support. Note: Pull Requests will run these tests and as a result may be skipped -locally if necessary. - -A sample `.envrc.example` file is included in the root directory which documents -which environment variables must be set to run the integration tests. Copy this -example file to `.envrc` at the root of the project, supply all the correct -values for each variable, source the file (`source .envrc`, or consider using -[direnv][]), and then run: - -``` -go test ./... -``` - -## Contributor License Agreements - -Open-source software licensing is a wonderful arrangement that benefits -everyone, but in an imperfect world, we all need to exercise some legal -prudence. In order to protect you, Google, and most of all, everyone who comes -to depend on these libraries, we require that all contributors sign our short -and human-readable Contributor License Agreement (CLA). We don't want to open -the door to patent trolls, predatory lawyers, or anyone else who isn't on board -with creating value and making the world a better place. We hope you will agree -that the CLA offers very important protection and is easy to understand. Take a -moment to read it carefully, and if you agree with what you read, please sign it -now. If you believe you've already signed the appropriate CLA already for this -or any other Google open-source project, you shouldn't have to do so again. You -can review your signed CLAs at -[cla.developers.google.com/clas](https://cla.developers.google.com/clas). - -First, check that you are signed in to a [Google -Account](https://accounts.google.com) that matches your [local Git email -address](https://help.github.com/articles/setting-your-email-in-git/). Then -choose one of the following: - -* If you are **an individual writing original source code** and **you own the - intellectual property**, sign the [Individual - CLA](https://developers.google.com/open-source/cla/individual). -* If you work for **a company that wants to allow you to contribute**, sign the - [Corporate CLA](https://developers.google.com/open-source/cla/corporate). - -You (and your authorized signer, if corporate) can sign the CLA -electronically. After that, we'll be able to accept your contributions. - -## Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of -fostering an open and welcoming community, we pledge to respect all people who -contribute through reporting issues, posting feature requests, updating -documentation, submitting pull requests or patches, and other activities. - -We are committed to making participation in this project a harassment-free -experience for everyone, regardless of level of experience, gender, gender -identity and expression, sexual orientation, disability, personal appearance, -body size, race, ethnicity, age, religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical or electronic -addresses, without explicit permission -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct. By adopting this Code of Conduct, project -maintainers commit themselves to fairly and consistently applying these -principles to every aspect of managing this project. Project maintainers who do -not follow or enforce the Code of Conduct may be permanently removed from the -project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by opening an issue or contacting one or more of the project -maintainers. - -This Code of Conduct is adapted from the [Contributor -Covenant](http://contributor-covenant.org), version 1.2.0, available at -[http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) - -[FUSE]: https://www.kernel.org/doc/html/latest/filesystems/fuse.html -[direnv]: https://direnv.net/ +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 63a15e2c3..000000000 --- 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/Dockerfile b/Dockerfile index 4c5dc5e45..c6c7c7ef2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ # Use the latest stable golang 1.x to compile to a binary FROM --platform=$BUILDPLATFORM golang:1 as build -WORKDIR /go/src/cloudsql-proxy +WORKDIR /go/src/cloud-sql-proxy COPY . . ARG TARGETOS @@ -23,10 +23,14 @@ ARG TARGETARCH RUN go get ./... RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags "-X main.metadataString=container" -o cloud_sql_proxy ./cmd/cloud_sql_proxy + go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container" # Final Stage -FROM gcr.io/distroless/static:nonroot -COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy +FROM gcr.io/distroless/static:nonroot@sha256:88a46f645e304fc0dcfbdacdfa338ce02d9890df5f936872243d553278deae92 + +LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" + +COPY --from=build --chown=nonroot /go/src/cloud-sql-proxy/cloud-sql-proxy /cloud-sql-proxy # set the uid as an integer for compatibility with runAsNonRoot in Kubernetes USER 65532 +ENTRYPOINT ["/cloud-sql-proxy"] diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 6e5c7022a..0fddd74ee 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -13,21 +13,24 @@ # limitations under the License. # Use the latest stable golang 1.x to compile to a binary -FROM --platform=$BUILDPLATFORM golang:1 as build +FROM --platform=$BUILDPLATFORM golang:1-alpine as build -WORKDIR /go/src/cloudsql-proxy +WORKDIR /go/src/cloud-sql-proxy COPY . . ARG TARGETOS ARG TARGETARCH RUN go get ./... -RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags "-X main.metadataString=container.alpine" -o cloud_sql_proxy ./cmd/cloud_sql_proxy +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.alpine" # Final stage -FROM alpine:3 -RUN apk add --no-cache \ +FROM alpine:3@sha256:59855d3dceb3ae53991193bd03301e082b2a7faa56a514b03527ae0ec2ce3a95 + +LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" + +RUN apk add --no-cache \ ca-certificates \ libc6-compat # Install fuse and allow enable non-root users to mount @@ -37,4 +40,5 @@ 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 +COPY --from=build --chown=nonroot /go/src/cloud-sql-proxy/cloud-sql-proxy /cloud-sql-proxy +ENTRYPOINT ["/cloud-sql-proxy"] diff --git a/Dockerfile.bookworm b/Dockerfile.bookworm new file mode 100644 index 000000000..68954c0e3 --- /dev/null +++ b/Dockerfile.bookworm @@ -0,0 +1,42 @@ +# Copyright 2024 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 --platform=$BUILDPLATFORM golang:1 as build + +WORKDIR /go/src/cloud-sql-proxy +COPY . . + +ARG TARGETOS +ARG TARGETARCH + +RUN go get ./... +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.bookworm" + +# Final stage +FROM gcr.io/cloud-marketplace-containers/google/debian12@sha256:8d11f62b1699a326f4ed1f457cae981dd555f5271d63d56b1ef2ae50fba23f4b + +LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" + +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/cloud-sql-proxy/cloud-sql-proxy /cloud-sql-proxy +ENTRYPOINT ["/cloud-sql-proxy"] diff --git a/Dockerfile.buster b/Dockerfile.bullseye similarity index 68% rename from Dockerfile.buster rename to Dockerfile.bullseye index a1dc2c4d8..6af54d905 100644 --- a/Dockerfile.buster +++ b/Dockerfile.bullseye @@ -15,18 +15,21 @@ # Use the latest stable golang 1.x to compile to a binary FROM --platform=$BUILDPLATFORM golang:1 as build -WORKDIR /go/src/cloudsql-proxy +WORKDIR /go/src/cloud-sql-proxy COPY . . ARG TARGETOS ARG TARGETARCH RUN go get ./... -RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -ldflags "-X main.metadataString=container.buster" -o cloud_sql_proxy ./cmd/cloud_sql_proxy +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.bullseye" # Final stage -FROM debian:buster +FROM gcr.io/cloud-marketplace-containers/google/debian11@sha256:308a07e11bfc84a20518eb34e5e8312ec2d9daf195151d73f71d8b1a53c560cc + +LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" + 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 @@ -35,4 +38,5 @@ 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 +COPY --from=build --chown=nonroot /go/src/cloud-sql-proxy/cloud-sql-proxy /cloud-sql-proxy +ENTRYPOINT ["/cloud-sql-proxy"] diff --git a/README.md b/README.md index 7d985f937..149660154 100644 --- a/README.md +++ b/README.md @@ -1,302 +1,662 @@ -# Cloud SQL Auth proxy +# Cloud SQL Auth Proxy [![CI][ci-badge]][ci-build] -[![Go Reference][pkg-badge]][pkg-docs] -The [Cloud SQL Auth proxy][proxy-page] is a binary that provides IAM-based -authorization and encryption when connecting to a Cloud SQL instance. +> [!WARNING] +> **Go versions 1.25.2 and 1.24.8 are NOT compatible with Cloud Auth Proxy.** +> +> An update to the Go version 1.25.2 and Go 1.24.8 breaks SAN verificaton. This is because Cloud SQL includes a trailing dot in the DNS name within the certificate's Subject Alternative Name (SAN), which the above Go versions reject as a malformed DNS name. +> +> For more details, please see the related Go issue: [crypto/x509: quadratic complexity when checking name constraints ](https://github.com/golang/go/issues/75715). -See the [Connecting Overview][connection-overview] page for more information on -connecting to a Cloud SQL instance, or the [About the proxy][about-proxy] page -for details on how the Cloud SQL proxy works. -Note: The Proxy *cannot* provide a network path to a Cloud SQL instance if one -is not already present (e.g., the proxy cannot access a VPC if it does not -already have access to it). +> [!IMPORTANT] +> +> The Cloud SQL Auth Proxy does not currently support Unix domain socket +> connections to MySQL 8.4 instances. This is due to a [known issue](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/issues/2317) +> involving the new default `caching_sha2_password` authentication plugin. + +The Cloud SQL Auth Proxy is a utility for ensuring secure connections to your +Cloud SQL instances. It provides IAM authorization, allowing you to control who +can connect to your instance through IAM permissions, and TLS 1.3 encryption, +without having to manage certificates. + +See the [Connecting Overview][connection-overview] page for more information on +connecting to a Cloud SQL instance, or the [About the Proxy][about-proxy] page +for details on how the Cloud SQL Proxy works. + +The Cloud SQL Auth Proxy has support for: + +- [Automatic IAM Authentication][iam-auth] (Postgres and MySQL only) +- Metrics ([Cloud Monitoring][], [Cloud Trace][], and [Prometheus][]) +- [HTTP Healthchecks][health-check-example] +- [Service account impersonation](#configuring-service-account-impersonation) +- Separate Dialer functionality released as the [Cloud SQL Go Connector][go connector] +- Configuration with [environment variables](#config-environment-variables) +- Fully POSIX-compliant flags + +If you're using Go, Java, Python, or Node.js, consider using the corresponding Cloud SQL +connector which does everything the Proxy does, but in process: + +- [Cloud SQL Go connector][go connector] +- [Cloud SQL Java connector][java connector] +- [Cloud SQL Python connector][python connector] +- [Cloud SQL Node.js connector][node connector] + +For users migrating from v1, see the [Migration Guide](migration-guide.md). +The [v1 README][v1 readme] is still available. + +> [!IMPORTANT] +> +> The Proxy does not configure the network between the VM it's running on +> and the Cloud SQL instance. You MUST ensure the Proxy can reach your Cloud SQL +> instance, either by deploying it in a VPC that has access to your Private IP +> instance, or by configuring Public IP. + +[cloud monitoring]: https://cloud.google.com/monitoring +[cloud trace]: https://cloud.google.com/trace +[prometheus]: https://prometheus.io/ +[go connector]: https://github.com/GoogleCloudPlatform/cloud-sql-go-connector +[java connector]: https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory +[python connector]: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector +[node connector]: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector +[v1 readme]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/blob/5f5b09b62eb6dfcaa58ce399d0131c1544bf813f/README.md ## Installation -For 64-bit Linux, run: +Check for the latest version on the [releases page][releases] and use the +following instructions for your OS and CPU architecture. -``` -VERSION=v1.21.0 # see Releases for other versions -wget "https://storage.googleapis.com/cloudsql-proxy/$VERSION/cloud_sql_proxy.linux.amd64" -O cloud_sql_proxy -chmod +x cloud_sql_proxy -``` +> [!NOTE] +> +> Starting with version `v2.17.1`, Windows binaries provided on the +> [releases page][releases] are signed with Google LLC certificates. -Releases for additional OS's and architectures and be found on the [releases -page][releases]. + +
+Linux amd64 -For alternative distributions, see below under [third party](#third-party). +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" -### Container Images +curl "$URL/cloud-sql-proxy.linux.amd64" -o cloud-sql-proxy -There are containerized versions of the proxy available from the following -Google Cloud Container Registry repositories: +chmod +x cloud-sql-proxy +``` -* `gcr.io/cloudsql-docker/gce-proxy` -* `us.gcr.io/cloudsql-docker/gce-proxy` -* `eu.gcr.io/cloudsql-docker/gce-proxy` -* `asia.gcr.io/cloudsql-docker/gce-proxy` +
-Each image is tagged with the associated proxy version. The following tags are -currently supported: +
+Linux 386 -* `$VERSION` - default image (recommended) -* `$VERSION-alpine` - uses [`alpine:3`](https://hub.docker.com/_/alpine) - as a base image (only supported from v1.17 up) -* `$VERSION-buster` - uses [`debian:buster`](https://hub.docker.com/_/debian) - as a base image (only supported from v1.17 up) +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" -We recommend using the latest version of the proxy and updating the version -regularly. However, we also recommend pinning to a specific tag and avoid the -latest tag. Note: the tagged version is only that of the proxy. Changes in base -images may break specific setups, even on non-major version increments. As such, -it's a best practice to test changes before deployment, and use automated -rollbacks to revert potential failures. +curl "$URL/cloud-sql-proxy.linux.386" -o cloud-sql-proxy -**NOTE**: As of v1.16, the default container image uses [distroless][]. If you -require a shell or similar tools, use the Alpine or Buster images listed above. +chmod +x cloud-sql-proxy +``` -[distroless]: https://github.com/GoogleContainerTools/distroless +
-### Install from Source +
+Linux arm64 -To install from source, ensure you have the latest version of [Go -installed](https://go.dev/doc/install). +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" -Then, simply run: +curl "$URL/cloud-sql-proxy.linux.arm64" -o cloud-sql-proxy -``` -go install github.com/GoogleCloudPlatform/cloudsql-proxy/cmd/cloud_sql_proxy@latest +chmod +x cloud-sql-proxy ``` -The `cloud_sql_proxy` will be placed in `$GOPATH/bin` or `$HOME/go/bin`. +
-## Usage +
+Linux arm -All the following invocations assume valid credentials are present in the -environment. The following examples all reference an `INSTANCE_CONNECTION_NAME`, -which takes the form: `myproject:myregion:myinstance`. To find the -`INSTANCE_CONNECTION_NAME`, run `gcloud sql instances describe ` -where `INSTANCE_NAME` is the name of the database instance. +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" -### TCP socket example +curl "$URL/cloud-sql-proxy.linux.arm" -o cloud-sql-proxy -``` bash -# Starts the proxy listening on 127.0.0.1:5432 -cloud_sql_proxy -instances==tcp:5432 +chmod +x cloud-sql-proxy ``` -``` bash -# Starts the proxy listening on port 5432 on *all* interfaces -cloud_sql_proxy -instances==tcp:0.0.0.0:5432 +
+ +
+Mac (Intel) + +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" + +curl "$URL/cloud-sql-proxy.darwin.amd64" -o cloud-sql-proxy + +chmod +x cloud-sql-proxy ``` -### Unix socket example +
-``` bash -# The proxy will mount a Unix domain socket at /cloudsql/ -# 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). -cloud_sql_proxy -dir=/cloudsql -instances= +
+Mac (Apple Silicon) + +```sh +# see Releases for other versions +URL="https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1" + +curl "$URL/cloud-sql-proxy.darwin.arm64" -o cloud-sql-proxy + +chmod +x cloud-sql-proxy ``` -### Private IP example +
+
+Windows x64 + +```sh +# see Releases for other versions +curl https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1/cloud-sql-proxy.x64.exe -o cloud-sql-proxy.exe ``` -cloud_sql_proxy -instances==tcp:5432 -ip_address_types=PRIVATE + +
+ +
+Windows x86 + +```sh +# see Releases for other versions +curl https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1/cloud-sql-proxy.x86.exe -o cloud-sql-proxy.exe ``` -In order to connect using Private IP, you must have access through your -project's VPC. For more details, see [Private IP Requirements][private-ip]. +
+ + +### Install from Source -## Credentials +To install from source, ensure you have the latest version of [Go installed](https://go.dev/doc/install). -The Cloud SQL proxy uses a Cloud IAM account to authorize connections against a -Cloud SQL instance. The proxy sources the credentials for these accounts in the -following order: +Then, simply run: -1. The `-credential_file` flag -2. The `-token` flag -3. The service account key at the path stored in the - `GOOGLE_APPLICATION_CREDENTIALS` environment variable. -4. The gcloud user credentials (set from `gcloud auth login`) -5. The [Application Default Credentials](https://cloud.google.com/docs/authentication/production) +```shell +go install github.com/GoogleCloudPlatform/cloud-sql-proxy/v2@latest +``` -Note: Any account connecting to a Cloud SQL database will need one of the -following IAM roles: +The `cloud-sql-proxy` will be placed in `$GOPATH/bin` or `$HOME/go/bin`. -- Cloud SQL Client (preferred) -- Cloud SQL Editor -- Cloud SQL Admin +## Usage -Or one may manually assign the following IAM permissions: +The following examples all reference an `INSTANCE_CONNECTION_NAME`, which takes +the form: `myproject:myregion:myinstance`. -- `cloudsql.instances.connect` -- `cloudsql.instances.get` +To find your Cloud SQL instance's `INSTANCE_CONNECTION_NAME`, visit the detail +page of your Cloud SQL instance in the console, or use `gcloud` with: -See [Roles and Permissions in Cloud SQL][roles-and-permissions] for details. +```shell +gcloud sql instances describe --format='value(connectionName)' +``` -When the proxy authenticates under the Compute Engine VM's default service +### Credentials + +The Cloud SQL Proxy uses a Cloud IAM principal to authorize connections against +a Cloud SQL instance. The Proxy sources the credentials using +[Application Default Credentials](https://cloud.google.com/docs/authentication/production). + +> [!NOTE] +> +> Any IAM principal connecting to a Cloud SQL database will need one of the +> following IAM roles: +> +> - Cloud SQL Client (preferred) +> - Cloud SQL Editor +> - Cloud SQL Admin +> +> Or one may manually assign the following IAM permissions: +> +> - `cloudsql.instances.connect` +> - `cloudsql.instances.get` +> +> See [Roles and Permissions in Cloud SQL][roles-and-permissions] for details. + +When the Proxy authenticates under the Compute Engine VM's default service account, the VM must have at least the `sqlservice.admin` API scope (i.e., "https://www.googleapis.com/auth/sqlservice.admin") and the associated project must have the SQL Admin API enabled. The default service account must also have at least writer or editor privileges to any projects of target SQL instances. +The Proxy also supports two flags related to credentials: -## CLI Flags - -The Cloud SQL Auth proxy takes a few arguments to configure what instances to connect -to and connection behavior. For a full list of flags supported by the proxy, -use `cloud_sql_proxy -help`. +- `--token` to use an OAuth2 token +- `--credentials-file` to use a service account key file -### Authentication Flags +### Basic Usage -#### `-credential_file` +To start the Proxy, use: -Specifies the path to a JSON [service account][service-account] key the proxy -uses to authorize or authenticate connections. +```shell +# starts the Proxy listening on localhost with the default database engine port +# For example: +# MySQL localhost:3306 +# Postgres localhost:5432 +# SQL Server localhost:1433 +./cloud-sql-proxy +``` -#### `-token` +The Proxy will automatically detect the default database engine's port and start +a corresponding listener. Production deployments should use the `--port` flag to +reduce startup time. -When set, the proxy uses this Bearer token for authorization. +The Proxy supports multiple instances: -#### `-enable_iam_login` +```shell +./cloud-sql-proxy +``` -Enables the proxy to use Cloud SQL IAM database authentication. This will cause -the proxy to use IAM account credentials for database user authentication. For -details, see [Overview of Cloud SQL IAM database authentication][iam-auth]. -NOTE: This feature only works with Postgres database instances. +### Configuring Port -### Connection Flags +To override the port, use the `--port` flag: -#### `-instances="project1:region:instance1,project3:region:instance1"` +```shell +# Starts a listener on localhost:6000 +./cloud-sql-proxy --port 6000 +``` -A comma-separated list of instances to open inside `-dir`. Also supports -exposing a TCP port and renaming the default Unix Domain Sockets; see examples -below. Same list can be provided via INSTANCES environment variable, in case -when both are provided - proxy will use command line flag. +When specifying multiple instances, the port will increment from the flag value: -**Example** +```shell +# Starts a listener on localhost:6000 for INSTANCE_CONNECTION_1 +# and localhost:6001 for INSTANCE_CONNECTION_NAME_2. +./cloud-sql-proxy --port 6000 +``` -Using TCP sockets: +To configure ports on a per instance basis, use the `port` query param: +```shell +# Starts a listener on localhost:5000 for the instance called "postgres" +# and starts a listener on localhost:6000 for the instance called "mysql" +./cloud-sql-proxy \ + 'myproject:my-region:postgres?port=5000' \ + 'myproject:my-region:mysql?port=6000' ``` -./cloud_sql_proxy -instances=my-project:us-central1:sql-inst=tcp:3306 & -mysql -u root -h 127.0.0.1 + +### Configuring Listening Address + +To override the choice of `localhost`, use the `--address` flag: + +```shell +# Starts a listener on all interfaces at port 5432 +./cloud-sql-proxy --address 0.0.0.0 ``` -Using Unix sockets: +To override address on a per-instance basis, use the `address` query param: +```shell +# Starts a listener on 0.0.0.0 for "postgres" at port 5432 +# and a listener on 10.0.0.1:3306 for "mysql" +./cloud-sql-proxy \ + 'myproject:my-region:postgres?address=0.0.0.0' \ + 'myproject:my-region:mysql?address=10.0.0.1" ``` -./cloud_sql_proxy -dir=/cloudsql -instances=my-project:us-central1:sql-inst & -mysql -u root -S /cloudsql/my-project:us-central1:sql-inst + +### Configuring Private IP + +By default, the Proxy attempts to connect to an instance's public IP. To enable +private IP, use: + +```shell +# Starts a listener connected to the private IP of the Cloud SQL instance. +# Note: there must be a network path present for this to work. +./cloud-sql-proxy --private-ip ``` -To specify a custom Unix socket name: +> [!IMPORTANT] +> +> The Proxy does not configure the network. You MUST ensure the Proxy can +> reach your Cloud SQL instance, either by deploying it in a VPC that has access +> to your Private IP instance, or by configuring Public IP. + +### Configuring Unix domain sockets +The Proxy also supports [Unix domain sockets](https://en.wikipedia.org/wiki/Unix_domain_socket). +To start the Proxy with Unix sockets, run: + +```shell +# Uses the directory "/mycooldir" to create a Unix socket +# For example, the following directory would be created: +# /mycooldir/myproject:myregion:myinstance +./cloud-sql-proxy --unix-socket /mycooldir ``` -./cloud_sql_proxy -dir=/cloudsql \ - -instances=my-project:us-central1:sql-inst=unix:custom_socket_name & -mysql -u root -S /cloudsql/custom_socket_name + +To configure a Unix domain socket on a per-instance basis, use the `unix-socket` +query param: + +```shell +# Starts a TCP listener on localhost:5432 for "postgres" +# and creates a Unix domain socket for "mysql": +# /cloudsql/myproject:my-region:mysql +./cloud-sql-proxy \ + myproject:my-region:postgres \ + 'myproject:my-region:mysql?unix-socket=/cloudsql' ``` -To specify a custom location for a Unix socket (overrides `-dir`): +> [!NOTE] +> +> The Proxy supports Unix domain sockets on recent versions of Windows, but +> replaces colons with periods: +> +> ```shell +> # Starts a Unix domain socket at the path: +> # C:\cloudsql\myproject.my-region.mysql +> ./cloud-sql-proxy --unix-socket C:\cloudsql myproject:my-region:mysql +> ``` + + +### Configuring IAM Database Authentication + +The Proxy supports [Automatic IAM Database Authentication][iam-auth] for MySQL +and Postgres instances, allowing IAM principal's to authenticate and connect +as database users. + +Make sure to configure your [Cloud SQL instance to allow IAM authentication][iam-auth-config] +and to [add your IAM principal as a database user][iam-auth-user]. +```shell +./cloud-sql-proxy --auto-iam-authn ``` -./cloud_sql_proxy -dir=/cloudsql \ - -instances=my-project:us-central1:sql-inst=unix:/my/custom/sql-socket & -mysql -u root -S /my/custom/sql-socket + +> [!IMPORTANT] +> +> Make sure to run the Proxy as the same IAM principal as the database user +> you want to log in as. Only the IAM principal that is attached to the +> [sourced credentials](#credentials) will be able to successfully log in +> via automatic IAM database authentication. +> +> When logging in using an IAM database user, Cloud SQL truncates usernames +> based on the engine type in order to not exceed character limits. +> PostgreSQL's username character limit is 63, while MySQL's is 32. +> +> Cloud SQL IAM database usernames are formatted in the following way: +> +> **Postgres**: +> * For an IAM user account, this is the user's email address. +> * For a service account, it is the service account's email without the +> `.gserviceaccount.com` domain suffix. +> +> **MySQL**: +> * For an IAM user account, this is the user's email address, +> without the `@` or domain name. For example, for `test-user@gmail.com`, +> the database user would be `test-user`. +> * For a service account, this is the service account's email address without +> the `@project-id.iam.gserviceaccount.com` suffix. + + +### Configuring Service Account Impersonation + +The Proxy supports [service account impersonation](https://cloud.google.com/iam/docs/impersonating-service-accounts). +This allows the Proxy to act as a different service account, which can be useful +for granting access to resources that are not accessible to the default IAM +principal. + +To use service account impersonation, you must have the +`iam.serviceAccounts.getAccessToken` permission on the IAM principal +impersonating another service account. You can grant this permission by assigning + the `roles/iam.serviceAccountTokenCreator` role to the IAM principal. + +To impersonate a service account, use the `--impersonate-service-account` flag: + +> [!NOTE] +> +> The impersonated service account must have the `Service Usage Consumer` and +`Cloud SQL Client` permissions. +> Additionally, to use IAM Authenticated users, add the `Cloud SQL Instance User` + permission. + + +```shell +# Starts a listener on localhost:5432 and impersonates the service account +# "my-other-sa@my-project.iam.gserviceaccount.com". +# The Proxy will use the credentials of the principal running the Proxy to +# generate a short-lived access token for the impersonated service account. +./cloud-sql-proxy --impersonate-service-account \ +my-other-sa@my-project.iam.gserviceaccount.com ``` -#### `-fuse` +### Using Advanced Disaster Recovery and DNS domain names to identify instances + +The proxy can be configured to use DNS to look up an instance. +Use a DNS name managed by Cloud SQL [Advanced Disaster Recovery](https://docs.cloud.google.com/sql/docs/mysql/use-advanced-disaster-recovery), +or a domain name that you manage. + +#### Using Advanced Recovery Write Endpoint DNS Name + +[Advanced Disaster Recovery](https://docs.cloud.google.com/sql/docs/mysql/use-advanced-disaster-recovery) +creates geographically distributed replicas of your Cloud SQL database instance. When you perform +switchover or failover on the database instance, the proxy will gracefully disconnect from the +old primary instance and reconnect to the new primary instance. -Requires access to `/dev/fuse` as well as the `fusermount` binary. An optional -`-fuse_tmp` flag can specify where to place temporary files. The directory -indicated by `-dir` is mounted. +Follow the instructions in [Connect using Write Endpoint](https://docs.cloud.google.com/sql/docs/mysql/connect-to-instance-using-write-endpoint) +to get the write endpoint DNS name for your primary instance. Then, use this write endpoint DNS +name to configure the proxy. -**Example** +#### Configure your DNS Records -Using `-fuse`, you do not need to specify instance names ahead of time: +The proxy may be configured to use DNS that you define as well. +Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server +or a private Google Cloud DNS Zone used by your application. + +**Note:** You are strongly discouraged from adding DNS records for your +Cloud SQL instances to a public DNS server. This would allow anyone on the +internet to discover the Cloud SQL instance name. + +For example: suppose you wanted to use the domain name +`prod-db.mycompany.example.com` to connect to your database instance +`my-project:region:my-instance`. You would create the following DNS record: + +- Record type: `TXT` +- Name: `prod-db.mycompany.example.com` – This is the domain name used by the application +- Value: `my-project:region:my-instance` – This is the instance name + +#### Configuring the Proxy + +Configure the Proxy with your DNS domain name instead of an instance connection +name: + +```sh +./cloud-sql-proxy prod-db.mycompany.example.com ``` -./cloud_sql_proxy -dir=/cloudsql -fuse & -mysql -u root -S /cloudsql/my-project:us-central1:sql-inst + +### Automatic fail-over using DNS domain names + +When the Proxy is configured using a domain name, it will +periodically check if the DNS record for an instance changes. When the Proxy +detects that the domain name refers to a different instance, it will +close all open connections to the old instance. Subsequent connection attempts +will be directed to the new instance. + +For example: suppose application is configured to connect using the +domain name `prod-db.mycompany.example.com`. Initially the corporate DNS +zone has a TXT record with the value `my-project:region:my-instance`. The +application establishes connections to the `my-project:region:my-instance` +Cloud SQL instance. + +Then, to reconfigure the application to use a different database +instance, change the value of the `prod-db.mycompany.example.com` DNS record +from `my-project:region:my-instance` to `my-project:other-region:my-instance-2` + +The Proxy detects the change to this DNS record. +Now, when the application connects to its database using the +domain name `prod-db.mycompany.example.com`, it will connect to the +`my-project:other-region:my-instance-2` Cloud SQL instance. + +The Proxy will automatically close all existing connections to +`my-project:region:my-instance`. This will force the connection pools to +establish new connections. Also, it may cause database queries in progress +to fail. + +The Proxy will poll for changes to the DNS name every 30 seconds by default. + +### Testing Connectivity + +The Proxy includes support for a connection test on startup. This test helps +ensure the Proxy can reach the associated instance and is a quick debugging +tool. The test will attempt to connect to the specified instance(s) and fail +if the instance is unreachable. If the test fails, the Proxy will exit with +a non-zero exit code. + +```shell +./cloud-sql-proxy --run-connection-test ``` -#### `-instances_metadata=metadata_key` +### Config file + +The Proxy supports a configuration file. Supported file types are TOML, JSON, +and YAML. Load the file with the `--config-file` flag: -Usable on [GCE](https://cloud.google.com/compute/docs/quickstart) only. The -given [GCE metadata](https://cloud.google.com/compute/docs/metadata) key will be -polled for a list of instances to open in `-dir`. The metadata key is relative -from `computeMetadata/v1/`. The format for the value is the same as the -'instances' flag. A hanging-poll strategy is used, meaning that changes to the -metadata value will be reflected in the `-dir` even while the proxy is running. -When an instance is removed from the list the corresponding socket will be -removed from `-dir` as well (unless it was also specified in `-instances`), but -any existing connections to this instance will NOT be terminated. +```shell +./cloud-sql-proxy --config-file /path/to/config.[toml|json|yaml] +``` -**Example** +The configuration file format supports all flags. The key names should match +the flag names. For example: +``` toml +# use instance-connection-name-0, instance-connection-name-1, etc. +# for multiple instances +instance-connection-name = "proj:region:inst" +auto-iam-authn = true +debug = true +debug-logs = true ``` -./cloud_sql_proxy -dir=/cloudsql \ - -instances_metadata instance/attributes/ & -mysql -u root -S /cloudsql/my-project:us-central1:sql-inst + +Run `./cloud-sql-proxy --help` for more details. See the full documentation +in [docs/cmd](docs/cmd). + +### Config environment variables + +The proxy supports configuration through environment variables. +Each environment variable uses "CSQL_PROXY" as a prefix and is +the uppercase version of the flag using underscores as word delimiters. + +For example, the `--auto-iam-authn` flag may be set with the environment variable +`CSQL_PROXY_AUTO_IAM_AUTHN`. + +An invocation of the Proxy using environment variables would look like the following: + +```shell +CSQL_PROXY_AUTO_IAM_AUTHN=true \ + ./cloud-sql-proxy ``` -Note: `-instances` and `-instances_metadata` may be used at the same time but -are not compatible with the `-fuse` flag. +Run `./cloud-sql-proxy --help` for more details. -#### `-max_connections` +### Configuring a Lazy Refresh -If provided, the maximum number of connections to establish before refusing new -connections. Defaults to 0 (no limit). +The `--lazy-refresh` flag configures the Proxy to retrieve connection info +lazily and as-needed. Otherwise, no background refresh cycle runs. This setting +is useful in environments where the CPU may be throttled outside of a request +context, e.g., Cloud Run, Cloud Functions, etc. -### Additional Flags +### Additional flags -#### `-ip_address_types=PUBLIC,PRIVATE` +To see a full list of flags, use: + +```shell +./cloud-sql-proxy --help +``` -A comma-delimited list of preferred IP types for connecting to an instance. For -example, setting this to PRIVATE will force the proxy to connect to instances -using an instance's associated private IP. Defaults to `PUBLIC,PRIVATE` -#### `-term_timeout=30s` +## Container Images -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. +There are containerized versions of the Proxy available from the following +[Artifact Registry](https://cloud.google.com/artifact-registry) repositories: -#### `-skip_failed_instance_config` +- `gcr.io/cloud-sql-connectors/cloud-sql-proxy` +- `us.gcr.io/cloud-sql-connectors/cloud-sql-proxy` +- `eu.gcr.io/cloud-sql-connectors/cloud-sql-proxy` +- `asia.gcr.io/cloud-sql-connectors/cloud-sql-proxy` -Setting this flag will prevent the proxy from terminating if any errors occur -during instance configuration. Please note that this means some instances may -fail to be set up correctly while others may work if the proxy restarts. +> [!NOTE] +> +> The above container images were migrated from Google Container Registry (deprecated) +> to Artifact Registry which is why they begin with the old naming pattern (`gcr.io`) -#### `-log_debug_stdout=true` +Each image is tagged with the associated Proxy version. The following tags are +currently supported: -This is to log non-error output to standard out instead of standard error. For -example, if you don't want connection related messages to log as errors, set -this flag to true. Defaults to false. +- `$VERSION` (default) +- `$VERSION-alpine` +- `$VERSION-bullseye` +- `$VERSION-bookworm` -#### `-structured_logs` + +The `$VERSION` is the Proxy version without the leading "v" (e.g., +`2.21.1`). -Writes all logging output as JSON with the following keys: severity, timestamp, caller, -message and optionally stacktrace. For example, the startup message looks like: +For example, to pull a particular version, use a command like: -```json -{"severity":"INFO","timestamp":"2020-10-12T07:20:50.52Z","caller":"cloud_sql_proxy/cloud_sql_proxy.go:510","message":"Using gcloud's active project: [my-project-id]"} +``` shell +# $VERSION is 2.21.1 +docker pull gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.21.1 ``` -#### `-use_http_health_check` + +We recommend pinning to a specific version tag and using automation with a CI pipeline +to update regularly. + +The default container image uses [distroless][] with a non-root user. If you +need a shell or related tools, use the Alpine or Debian-based container images +(bullseye or bookworm) listed above. -Enables HTTP health checks for the proxy, including startup, liveness, and readiness probing. -Requires that you configure the Kubernetes container with HTTP probes ([instructions][health-check-example]). +[distroless]: https://github.com/GoogleContainerTools/distroless -#### `-health_check_port=8090` +### Working with Docker and the Proxy -Specifies the port that the health check server listens and serves on. Defaults to 8090. +The containers have the proxy as an `ENTRYPOINT` so, to use the proxy from a +container, all you need to do is specify options using the command, and expose +the proxy's internal port to the host. For example, you can use: + +```shell +docker run --publish : \ + gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest \ + --address "0.0.0.0" --port +``` + +You'll need the `--address "0.0.0.0"` so that the proxy doesn't only listen for +connections originating from *within* the container. + +You will need to authenticate using one of the methods outlined in the +[credentials](#credentials) section. If using a credentials file you must mount +the file and ensure that the non-root user that runs the proxy has *read access* +to the file. These alternatives might help: + +1. Change the group of your local file and add read permissions to the group +with `chgrp 65532 key.json && chmod g+r key.json`. +1. If you can't control your file's group, you can directly change the public +permissions of your file by doing `chmod o+r key.json`. + +> [!WARNING] +> +> This can be insecure because it allows any user in the host system to read +> the credential file which they can use to authenticate to services in GCP. + +For example, a full command using a JSON credentials file might look like + +```shell +docker run \ + --publish : \ + --mount type=bind,source="$(pwd)"/sa.json,target=/config/sa.json \ + gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest \ + --address 0.0.0.0 \ + --port \ + --credentials-file /config/sa.json +``` ## Running as a Kubernetes Sidecar @@ -312,7 +672,7 @@ the Cloud SQL Auth Proxy would look like: ``` ALL_PROXY=socks5://localhost:8000 \ HTTPS_PROXY=socks5://localhost:8000 \ - cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 + cloud-sql-proxy ``` The `ALL_PROXY` environment variable specifies the proxy for all TCP @@ -326,13 +686,154 @@ when you want to proxy this traffic. Otherwise, it is optional. See [`http.ProxyFromEnvironment`](https://pkg.go.dev/net/http@go1.17.3#ProxyFromEnvironment) for possible values. +## Support for Metrics and Tracing + +The Proxy supports [Cloud Monitoring][], [Cloud Trace][], and [Prometheus][]. + +Supported metrics include: + +- `cloudsqlconn/dial_latency`: The distribution of dialer latencies (ms) +- `cloudsqlconn/open_connections`: The current number of open Cloud SQL + connections +- `cloudsqlconn/dial_failure_count`: The number of failed dial attempts +- `cloudsqlconn/refresh_success_count`: The number of successful certificate + refresh operations +- `cloudsqlconn/refresh_failure_count`: The number of failed refresh + operations. + +Supported traces include: + +- `cloud.google.com/go/cloudsqlconn.Dial`: The dial operation including + refreshing an ephemeral certificate and connecting the instance +- `cloud.google.com/go/cloudsqlconn/internal.InstanceInfo`: The call to retrieve + instance metadata (e.g., database engine type, IP address, etc) +- `cloud.google.com/go/cloudsqlconn/internal.Connect`: The connection attempt + using the ephemeral certificate +- SQL Admin API client operations + +To enable Cloud Monitoring and Cloud Trace, use the `--telemetry-project` flag +with the project where you want to view metrics and traces. To configure the +metrics prefix used by Cloud Monitoring, use the `--telemetry-prefix` flag. When +enabling telemetry, both Cloud Monitoring and Cloud Trace are enabled. To +disable Cloud Monitoring, use `--disable-metrics`. To disable Cloud Trace, use +`--disable-traces`. + +To enable Prometheus, use the `--prometheus` flag. This will start an HTTP +server on localhost with a `/metrics` endpoint. The Prometheus namespace may +optionally be set with `--prometheus-namespace`. + +## Debug logging + +To enable debug logging to report on internal certificate refresh operations, +use the `--debug-logs` flag. Typical use of the Proxy should not require debug +logs, but if you are surprised by the Proxy's behavior, debug logging should +provide insight into internal operations and can help when reporting issues. + +## Localhost Admin Server + +The Proxy includes support for an admin server on localhost. By default, the +the admin server is not enabled. To enable the server, pass the --debug or +--quitquitquit flag. This will start the server on localhost at port 9091. +To change the port, use the --admin-port flag. + +When --debug is set, the admin server enables Go's profiler available at +/debug/pprof/. + +See the [documentation on pprof][pprof] for details on how to use the +profiler. + +When --quitquitquit is set, the admin server adds an endpoint at +/quitquitquit. The admin server exits gracefully when it receives a GET or POST +request at /quitquitquit. + +[pprof]: https://pkg.go.dev/net/http/pprof. + +## Frequently Asked Questions + +### Why would I use the Proxy? + +The Proxy is a convenient way to control access to your database using IAM +permissions while ensuring a secure connection to your Cloud SQL instance. When +using the Proxy, you do not have to manage database client certificates, +configured Authorized Networks, or ensure clients connect securely. The Proxy +handles all of this for you. + +### How should I use the Proxy? + +The Proxy is a gateway to your Cloud SQL instance. Clients connect to the Proxy +over an unencrypted connection and are authorized using the environment's IAM +principal. The Proxy then encrypts the connection to your Cloud SQL instance. + +Because client connections are not encrypted and authorized using the +environment's IAM principal, we recommend running the Proxy on the same VM or +Kubernetes pod as your application and using the Proxy's default behavior of +allowing connections from only the local network interface. This is the most +secure configuration: unencrypted traffic does not leave the VM, and only +connections from applications on the VM are allowed. + +Here are some common examples of how to run the Proxy in different environments: + +- [Connect to Cloud SQL for MySQL from your local computer][local-quickstart] +- [Connect to Cloud SQL for MySQL from Google Kubernetes Engine][gke-quickstart] + +[local-quickstart]: https://cloud.google.com/sql/docs/mysql/connect-instance-local-computer +[gke-quickstart]: https://cloud.google.com/sql/docs/mysql/connect-instance-kubernetes + +### Why can't the Proxy connect to my private IP instance? + +The Proxy does not configure the network between the VM it's running on and the +Cloud SQL instance. You MUST ensure the Proxy can reach your Cloud SQL +instance, either by deploying it in a VPC that has access to your Private IP +instance, or by configuring Public IP. + +### Should I use the Proxy for large deployments? + +We recommend deploying the Proxy on the host machines that are running the +application. However, large deployments may exceed the request quota for the SQL +Admin API . If your Proxy reports request quota errors, we recommend deploying +the Proxy with a connection pooler like [pgbouncer][] or [ProxySQL][]. For +details, see [Running the Cloud SQL Proxy as a Service][service-example]. + +### Can I share the Proxy across multiple applications? + +Instead of using a single Proxy across multiple applications, we recommend using +one Proxy instance for every application process. The Proxy uses the context's +IAM principal and so have a 1-to-1 mapping between application and IAM principal +is best. If multiple applications use the same Proxy instance, then it becomes +unclear from an IAM perspective which principal is doing what. + +### How do I verify the shasum of a downloaded Proxy binary? + +After downloading a binary from the releases page, copy the sha256sum value +that corresponds with the binary you chose. + +Then run this command (make sure to add the asterisk before the file name): + +``` shell +echo ' *' | shasum -c +``` + +For example, after downloading the v2.1.0 release of the Linux AMD64 Proxy, you +would run: + +``` shell +$ echo "547b24faf0dfe5e3d16bbc9f751dfa6b34dfd5e83f618f43a2988283de5208f2 *cloud-sql-proxy" | shasum -c +cloud-sql-proxy: OK +``` + +If you see `OK`, the binary is a verified match. + + +[pgbouncer]: https://www.pgbouncer.org/ +[proxysql]: https://www.proxysql.com/ + ## Reference Documentation - [Cloud SQL][cloud-sql] -- [Cloud SQL Auth proxy Documentation][proxy-page] -- [Cloud SQL Auth proxy Quickstarts][quickstarts] +- [Cloud SQL Auth Proxy Documentation][proxy-page] +- [Cloud SQL Auth Proxy Quickstarts][quickstarts] - [Cloud SQL Code Samples][code-samples] -- [Cloud SQL Auth proxy Package Documentation][pkg-docs] +- [Cloud SQL Auth Proxy Package Documentation][pkg-docs] ## Support policy @@ -341,66 +842,43 @@ for possible values. This project uses [semantic versioning](https://semver.org/), and uses the following lifecycle regarding support for a major version: -**Active** - Active versions get all new features and security fixes (that +- **Active** - Active versions get all new features and security fixes (that wouldn’t otherwise introduce a breaking change). New major versions are guaranteed to be "active" for a minimum of 1 year. -**Deprecated** - Deprecated versions continue to receive security and critical -bug fixes, but do not receive new features. Deprecated versions will be publicly -supported for 1 year. -**Unsupported** - Any major version that has been deprecated for >=1 year is -considered publicly unsupported. - -### Supported Go Versions -We test and support at least the latest 3 Go versions. Changes in supported Go -versions will be considered a minor change, and will be noted in the release notes. +- **Maintenance** - Maintenance versions continue to receive security and critical +bug fixes, but do not receive new features. ### Release cadence -The Cloud SQL Auth proxy aims for a minimum monthly release cadence. If no new + +The Cloud SQL Auth Proxy aims for a minimum monthly release cadence. If no new features or fixes have been added, a new PATCH version with the latest dependencies is released. +We support releases for 1 year from the release date. + ## Contributing Contributions are welcome. Please, see the [CONTRIBUTING][contributing] document for details. Please note that this project is released with a Contributor Code of Conduct. -By participating in this project you agree to abide by its terms. See +By participating in this project you agree to abide by its terms. See [Contributor Code of Conduct][code-of-conduct] for more information. -## Third Party - -__WARNING__: _These distributions are not officially supported by Google._ - -### Homebrew - -There is Homebrew formula for Cloud SQL Auth proxy [here](https://github.com/tclass/homebrew-cloud_sql_proxy). - -### Kubernetes Cluster Service using Helm - -Follow these [instructions](https://github.com/rimusz/charts/tree/master/stable/gcloud-sqlproxy). - -This chart creates a Deployment and a Service, but we recommend deploying the -proxy as a sidecar container in your pods. - -### .Net Proxy Wrapper (Nuget Package) - -Install via Nuget, follow these -[instructions](https://github.com/expert1-pty-ltd/cloudsql-proxy#install-via-nuget). - - [about-proxy]: https://cloud.google.com/sql/docs/mysql/sql-proxy [ci-badge]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/actions/workflows/tests.yaml/badge.svg?event=push [ci-build]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/actions/workflows/tests.yaml?query=event%3Apush+branch%3Amain [cloud-sql]: https://cloud.google.com/sql [code-samples]: https://cloud.google.com/sql/docs/mysql/samples -[code-of-conduct]: CONTRIBUTING.md#contributor-code-of-conduct +[code-of-conduct]: CODE_OF_CONDUCT.md [connect-to-k8s]: https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine [connection-overview]: https://cloud.google.com/sql/docs/mysql/connect-overview [contributing]: CONTRIBUTING.md [health-check-example]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/main/examples/k8s-health-check#cloud-sql-proxy-health-checks -[iam-auth]: https://cloud.google.com/sql/docs/postgres/authentication +[iam-auth]: https://cloud.google.com/sql/docs/postgres/iam-authentication#auto-iam-auth +[iam-auth-config]: https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance +[iam-auth-user]: https://cloud.google.com/sql/docs/postgres/add-manage-iam-users#creating-a-database-user [pkg-badge]: https://pkg.go.dev/badge/github.com/GoogleCloudPlatform/cloudsql-proxy.svg [pkg-docs]: https://pkg.go.dev/github.com/GoogleCloudPlatform/cloudsql-proxy [private-ip]: https://cloud.google.com/sql/docs/mysql/private-ip#requirements_for_private_ip @@ -410,4 +888,4 @@ Install via Nuget, follow these [roles-and-permissions]: https://cloud.google.com/sql/docs/mysql/roles-and-permissions [service-account]: https://cloud.google.com/iam/docs/service-accounts [sidecar-example]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/master/examples/k8s-sidecar#run-the-cloud-sql-proxy-as-a-sidecar -[source-install]: docs/install-from-source.md +[service-example]: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/main/examples/k8s-service diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..8b58ae9c0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). + +The Google Security Team will respond within 5 working days of your report on g.co/vulnz. + +We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..9d8f83ce8 --- /dev/null +++ b/build.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash + +# Copyright 2025 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. + +# Set SCRIPT_DIR to the current directory of this file. +SCRIPT_DIR=$(cd -P "$(dirname "$0")" >/dev/null 2>&1 && pwd) +SCRIPT_FILE="${SCRIPT_DIR}/$(basename "$0")" + +## +## Local Development +## +## These functions should be used to run the local development process +## + +## clean - Cleans the build output +function clean() { + if [[ -d '.tools' ]] ; then + rm -rf .tools + fi +} + +## build - Builds the project without running tests. +function build() { + go build -o ./cloud-sql-proxy main.go +} + +## test - Runs local unit tests. +function test() { + go test -v -race -cover -short ./... +} + +## e2e - Runs end-to-end integration tests. +function e2e() { + if [[ ! -f .envrc ]] ; then + write_e2e_env .envrc + fi + source .envrc + e2e_ci +} + +# e2e_ci - Run end-to-end integration tests in the CI system. +# This assumes that the secrets in the env vars are already set. +function e2e_ci() { + go test -race -v ./... | tee test_results.txt +} + +function get_golang_tool() { + name="$1" + github_repo="$2" + package="$3" + + # Download goimports tool + version=$(curl -s "https://api.github.com/repos/$github_repo/tags" | jq -r '.[].name' | head -n 1) + mkdir -p "$SCRIPT_DIR/.tools" + cmd="$SCRIPT_DIR/.tools/$name" + versioned_cmd="$SCRIPT_DIR/.tools/$name-$version" + if [[ ! -f "$versioned_cmd" ]] ; then + GOBIN="$SCRIPT_DIR/.tools" go install "$package@$version" + mv "$cmd" "$versioned_cmd" + if [[ -f "$cmd" ]] ; then + unlink "$cmd" + fi + ln -s "$versioned_cmd" "$cmd" + fi +} + +## fix - Fixes code format. +function fix() { + # run code formatting + get_golang_tool 'goimports' 'golang/tools' 'golang.org/x/tools/cmd/goimports' + ".tools/goimports" -w . + go mod tidy + go fmt ./... + + # Generate CMD docs + go run ./cmd/gendocs/gen_cloud-sql-proxy_docs.go +} + +## lint - runs the linters +function lint() { + # run lint checks + get_golang_tool 'golangci-lint' 'golangci/golangci-lint' 'github.com/golangci/golangci-lint/v2/cmd/golangci-lint' + ".tools/golangci-lint" run --timeout 3m + + # Check the commit includes a go.mod that is fully + # up to date. + fix + if [[ -d "$SCRIPT_DIR/.git" ]] ; then + git diff --exit-code + fi +} + +# lint_ci - runs lint in the CI build job, exiting with an error code if lint fails. +function lint_ci() { + lint # run lint + git diff --exit-code # fail if any files changed +} + +## deps - updates project dependencies to latest +function deps() { + go get -u ./... + go get -t -u ./... + + # Update the image label in the dockerfiles + for n in Dockerfile Dockerfile.* ; do + dockerfile_from_deps "$n" + done +} + +# find +function dockerfile_from_deps() { + # FROM gcr.io/distroless/static:nonroot@sha256:627d6c5a23ad24e6bdff827f16c7b60e0289029b0c79e9f7ccd54ae3279fb45f + # curl -X GET https://gcr.io/v2/distroless/static/manifests/nonroot + file=$1 + + # Get the last FROM statement from the dockerfile + # those ar + fromLine=$(grep "FROM" $1 | tail -n1) + imageUrl="${fromLine#FROM *}" + + # If the image URL does not contain a hash, then don't do anything. + if [[ $imageUrl != *@* ]] ; then + echo "Image does not contain a digest, ignoring" + return + fi + + oldDigest="${imageUrl#*@}" #after the '@' + imageWithoutHash="${imageUrl%%@sha256*}" #before the '@sha256' + imageName="${imageWithoutHash%%:*}" #before the ':' + + imageLabel="${imageWithoutHash#*:}" #after the ':' + # If none found, use "latest" as the label + if [[ "$imageLabel" == "$imageName" ]] ; then + imageLabel=latest + fi + + imageRepo="${imageName%%/*}" #first part of the image name path, may be a repo hostname + if [[ "$imageRepo" == *.* ]]; then + imageName="${imageName#*/}" # trim repo name host from imageName + manifestUrl="https://${imageRepo}/v2/${imageName}/manifests/${imageLabel}" + digest=$(curl -X GET "$manifestUrl" | \ + jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + + else + # registry-1.docker.io requires a token + docker_io_token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" | jq -r .token) + manifestUrl="https://registry-1.docker.io/v2/${imageName}/manifests/${imageLabel}" + digest=$(curl -s -H "Authorization: Bearer $docker_io_token" \ + -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ + https://registry-1.docker.io/v2/library/alpine/manifests/3 | \ + jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + fi + + if [[ "$oldDigest" == "$digest" ]] ; then + echo "No update to image to $file" + else + echo "Updating docker image to $file to $digest" + set -x + sed -i.bak -e "s/$oldDigest/$digest/g" "$file" + fi + if [[ -f "$file.bak" ]] ; then + rm "$file.bak" + fi + +} + +# write_e2e_env - Loads secrets from the gcloud project and writes +# them to target/e2e.env to run e2e tests. +function write_e2e_env(){ + # All secrets used by the e2e tests in the form = + secret_vars=( + MYSQL_CONNECTION_NAME=MYSQL_CONNECTION_NAME + MYSQL_USER=MYSQL_USER + MYSQL_PASS=MYSQL_PASS + MYSQL_DB=MYSQL_DB + MYSQL_MCP_CONNECTION_NAME=MYSQL_MCP_CONNECTION_NAME + MYSQL_MCP_PASS=MYSQL_MCP_PASS + POSTGRES_CONNECTION_NAME=POSTGRES_CONNECTION_NAME + POSTGRES_USER=POSTGRES_USER + POSTGRES_USER_IAM=POSTGRES_USER_IAM + POSTGRES_PASS=POSTGRES_PASS + POSTGRES_DB=POSTGRES_DB + POSTGRES_CAS_CONNECTION_NAME=POSTGRES_CAS_CONNECTION_NAME + POSTGRES_CAS_PASS=POSTGRES_CAS_PASS + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME=POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + POSTGRES_CUSTOMER_CAS_PASS=POSTGRES_CUSTOMER_CAS_PASS + POSTGRES_CUSTOMER_CAS_DOMAIN_NAME=POSTGRES_CUSTOMER_CAS_DOMAIN_NAME + POSTGRES_MCP_CONNECTION_NAME=POSTGRES_MCP_CONNECTION_NAME + POSTGRES_MCP_PASS=POSTGRES_MCP_PASS + SQLSERVER_CONNECTION_NAME=SQLSERVER_CONNECTION_NAME + SQLSERVER_USER=SQLSERVER_USER + SQLSERVER_PASS=SQLSERVER_PASS + SQLSERVER_DB=SQLSERVER_DB + IMPERSONATED_USER=IMPERSONATED_USER + ) + + if [[ -z "$TEST_PROJECT" ]] ; then + echo "Set TEST_PROJECT environment variable to the project containing" + echo "the e2e test suite secrets." + exit 1 + fi + + local_user=$(gcloud auth list --format 'value(account)' | tr -d '\n') + + echo "Getting test secrets from $TEST_PROJECT into $1" + { + for env_name in "${secret_vars[@]}" ; do + env_var_name="${env_name%%=*}" + secret_name="${env_name##*=}" + set -x + val=$(gcloud secrets versions access latest --project "$TEST_PROJECT" --secret="$secret_name") + echo "export $env_var_name='$val'" + done + + # Set IAM User env vars to the local gcloud user + echo "export MYSQL_IAM_USER='${local_user%%@*}'" + echo "export POSTGRES_USER_IAM='$local_user'" + } > "$1" + +} + +## build_image - Builds and pushes the proxy container image using local source. +## Usage: ./build.sh build_image [image-url] +function build_image() { + local image_url="${1:-}" + local push_arg="" + + if [[ -n "$image_url" ]]; then + push_arg="--push" + echo "Preparing to build and push proxy image: $image_url" + else + echo "Preparing to build proxy image (no push)..." + push_arg="--load" + image_url="cloud-sql-proxy:local" + fi + + function cleanup_build() { + rm -f cloud-sql-proxy Dockerfile.local + } + trap cleanup_build EXIT + + echo "Building binary locally..." + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container" -o cloud-sql-proxy + + echo "Creating temporary Dockerfile..." + cat > Dockerfile.local < " + echo + echo "Commands to assist with local development and CI builds." + echo + echo "Commands:" + echo + grep -e '^##' "$SCRIPT_FILE" | sed -e 's/##/ /' +} + +set -euo pipefail + +# Check CLI Arguments +if [[ "$#" -lt 1 ]] ; then + help + exit 1 +fi + +cd "$SCRIPT_DIR" + +"$@" + diff --git a/cloudsql/cloudsql.go b/cloudsql/cloudsql.go new file mode 100644 index 000000000..d4c1522b2 --- /dev/null +++ b/cloudsql/cloudsql.go @@ -0,0 +1,44 @@ +// 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 +} + +// Logger is the interface used throughout the project for logging. +type Logger interface { + // Debugf is for reporting additional information about internal operations. + Debugf(format string, args ...interface{}) + // Infof is for reporting informational messages. + Infof(format string, args ...interface{}) + // Errorf is for reporting errors. + Errorf(format string, args ...interface{}) +} 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 9749cc943..000000000 --- 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/cmd/cloud_sql_proxy/internal/healthcheck" - "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/certs" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/fuse" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/limits" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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/internal/healthcheck/healthcheck.go b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go deleted file mode 100644 index cd57758eb..000000000 --- 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/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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 c8ca8277a..000000000 --- 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/cmd/cloud_sql_proxy/internal/healthcheck" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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 42917a5da..000000000 --- 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/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/fuse" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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 a8d8c37f3..000000000 --- a/cmd/cloud_sql_proxy/proxy_test.go +++ /dev/null @@ -1,295 +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 - }{ - { - desc: "setting -fuse and -dir", - dir: "dir", - useFuse: true, - instances: nil, - instancesSrc: "", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -fuse", - dir: "", - useFuse: true, - instances: nil, - instancesSrc: "", - wantErr: true, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -fuse, -dir, and -instances", - dir: "dir", - useFuse: true, - instances: []string{"proj:reg:x"}, - instancesSrc: "", - wantErr: true, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -fuse, -dir, and -instances_metadata", - dir: "dir", - useFuse: true, - instances: nil, - instancesSrc: "md", - wantErr: true, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -dir and -instances (unix socket)", - dir: "dir", - useFuse: false, - instances: []string{"proj:reg:x"}, - instancesSrc: "", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - // tests for the case where invalid configs can still exist, when skipped - desc: "setting -dir and -instances (unix socket) w/ something invalid", - dir: "dir", - useFuse: false, - instances: []string{"proj:reg:x", "INVALID_PROJECT_STRING"}, - instancesSrc: "", - wantErr: false, - skipFailedInstanceConfig: true, - }, - { - desc: "Seting -instance (unix socket)", - dir: "", - useFuse: false, - instances: []string{"proj:reg:x"}, - instancesSrc: "", - wantErr: true, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -instance (tcp socket)", - dir: "", - useFuse: false, - instances: []string{"proj:reg:x=tcp:1234"}, - instancesSrc: "", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -instance (tcp socket) and -instances_metadata", - dir: "", - useFuse: false, - instances: []string{"proj:reg:x=tcp:1234"}, - instancesSrc: "md", - wantErr: true, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -dir, -instance (tcp socket), and -instances_metadata", - dir: "dir", - useFuse: false, - instances: []string{"proj:reg:x=tcp:1234"}, - instancesSrc: "md", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -dir, -instance (unix socket), and -instances_metadata", - dir: "dir", - useFuse: false, - instances: []string{"proj:reg:x"}, - instancesSrc: "md", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -dir and -instances_metadata", - dir: "dir", - useFuse: false, - instances: nil, - instancesSrc: "md", - wantErr: false, - skipFailedInstanceConfig: false, - }, - { - desc: "setting -instances_metadata", - dir: "", - useFuse: false, - instances: nil, - instancesSrc: "md", - wantErr: true, - skipFailedInstanceConfig: false, - }, - } { - t.Run(v.desc, func(t *testing.T) { - // fuse is not supported in CI for darwin - if runtime.GOOS == "darwin" && v.useFuse { - t.Skip("skipping Darwin and FUSE test") - } - // fuse and unix sockets are not supported on windows - if runtime.GOOS == "windows" && (v.useFuse || v.dir != "") { - t.Skip("skipping Windows and FUSE/Unix socket test") - } - 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) - } - return - } - 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 bf80719ba..000000000 --- a/cmd/cloud_sql_proxy/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.32.0-dev diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 000000000..8b252dff5 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,142 @@ +// Copyright 2024 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 ( + "testing" +) + +func assert[T comparable](t *testing.T, want, got T) { + t.Helper() + if got != want { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestNewCommandWithConfigFile(t *testing.T) { + tcs := []struct { + desc string + args []string + setup func() + assert func(t *testing.T, c *Command) + }{ + { + desc: "toml config file", + args: []string{"--config-file", "testdata/config-toml.toml"}, + setup: func() {}, + assert: func(t *testing.T, c *Command) { + assert(t, 1, len(c.conf.Instances)) + assert(t, true, c.conf.Debug) + assert(t, 5555, c.conf.Port) + assert(t, true, c.conf.DebugLogs) + assert(t, true, c.conf.IAMAuthN) + }, + }, + { + desc: "yaml config file", + args: []string{"--config-file", "testdata/config-yaml.yaml"}, + setup: func() {}, + assert: func(t *testing.T, c *Command) { + assert(t, 1, len(c.conf.Instances)) + assert(t, true, c.conf.Debug) + }, + }, + { + desc: "json config file", + args: []string{"--config-file", "testdata/config-json.json"}, + setup: func() {}, + assert: func(t *testing.T, c *Command) { + assert(t, 1, len(c.conf.Instances)) + assert(t, true, c.conf.Debug) + }, + }, + { + desc: "config file with two instances", + args: []string{"--config-file", "testdata/two-instances.toml"}, + setup: func() {}, + assert: func(t *testing.T, c *Command) { + assert(t, 2, len(c.conf.Instances)) + }, + }, + { + desc: "instance argument overrides env config precedence", + args: []string{"proj:region:inst"}, + setup: func() { + t.Setenv("CSQL_PROXY_INSTANCE_CONNECTION_NAME", "p:r:i") + }, + assert: func(t *testing.T, c *Command) { + assert(t, "proj:region:inst", c.conf.Instances[0].Name) + }, + }, + { + desc: "instance env overrides config file precedence", + args: []string{"--config-file", "testdata/config.json"}, + setup: func() { + t.Setenv("CSQL_PROXY_INSTANCE_CONNECTION_NAME", "p:r:i") + }, + assert: func(t *testing.T, c *Command) { + assert(t, "p:r:i", c.conf.Instances[0].Name) + }, + }, + { + desc: "flag overrides env config precedence", + args: []string{"proj:region:inst", "--debug"}, + setup: func() { + t.Setenv("CSQL_PROXY_DEBUG", "false") + }, + assert: func(t *testing.T, c *Command) { + assert(t, true, c.conf.Debug) + }, + }, + { + desc: "flag overrides config file precedence", + args: []string{ + "proj:region:inst", + "--config-file", "testdata/config.toml", + "--debug", + }, + setup: func() {}, + assert: func(t *testing.T, c *Command) { + assert(t, true, c.conf.Debug) + }, + }, + { + desc: "env overrides config file precedence", + args: []string{ + "proj:region:inst", + "--config-file", "testdata/config.toml", + }, + setup: func() { + t.Setenv("CSQL_PROXY_DEBUG", "false") + }, + assert: func(t *testing.T, c *Command) { + assert(t, false, c.conf.Debug) + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + tc.setup() + + cmd, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + tc.assert(t, cmd) + }) + } +} diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 000000000..fd383fecd --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,62 @@ +// 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. + +package cmd + +import ( + "errors" +) + +var ( + errSigInt = &exitError{ + Err: errors.New("SIGINT signal received"), + Code: 130, + } + + errSigTerm = &exitError{ + Err: errors.New("SIGTERM signal received"), + Code: 143, + } + + errSigTermZero = &exitError{ + Err: errors.New("SIGTERM signal received"), + Code: 0, + } + + errQuitQuitQuit = &exitError{ + Err: errors.New("/quitquitquit received request"), + Code: 0, // This error guarantees a clean exit. + } +) + +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/gendocs/gen_cloud-sql-proxy_docs.go b/cmd/gendocs/gen_cloud-sql-proxy_docs.go new file mode 100644 index 000000000..1829d54ea --- /dev/null +++ b/cmd/gendocs/gen_cloud-sql-proxy_docs.go @@ -0,0 +1,51 @@ +// Copyright 2024 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 ( + "fmt" + "os" + "path/filepath" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd" + "github.com/spf13/cobra/doc" +) + +func main() { + if len(os.Args) > 2 { + fmt.Fprintf(os.Stderr, "usage: %s [output directory]\n", os.Args[0]) + os.Exit(1) + } + + path := "docs/cmd" + if len(os.Args) == 2 { + path = os.Args[1] + } + + outDir, err := filepath.Abs(path) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get output directory: %v\n", err) + os.Exit(1) + } + + // Set environment variables used so the output is consistent, + // regardless of where we run. + os.Setenv("TMPDIR", "/tmp") + + cloudSQLProxy := cmd.NewCommand() + cloudSQLProxy.Execute() + cloudSQLProxy.DisableAutoGenTag = true + doc.GenMarkdownTree(cloudSQLProxy.Command, outDir) +} diff --git a/cmd/options.go b/cmd/options.go new file mode 100644 index 000000000..aceb01843 --- /dev/null +++ b/cmd/options.go @@ -0,0 +1,107 @@ +// Copyright 2024 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 "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + +// Option is a function that configures a Command. +type Option func(*Command) + +// WithLogger overrides the default logger. +func WithLogger(l cloudsql.Logger) Option { + return func(c *Command) { + c.logger = l + } +} + +// WithDialer configures the Command to use the provided dialer to connect to +// Cloud SQL instances. +func WithDialer(d cloudsql.Dialer) Option { + return func(c *Command) { + c.dialer = d + } +} + +// WithFuseDir mounts a directory at the path using FUSE to access Cloud SQL +// instances. +func WithFuseDir(dir string) Option { + return func(c *Command) { + c.conf.FUSEDir = dir + } +} + +// WithFuseTempDir sets the temp directory where Unix sockets are created with +// FUSE +func WithFuseTempDir(dir string) Option { + return func(c *Command) { + c.conf.FUSETempDir = dir + } +} + +// WithMaxConnections sets the maximum allowed number of connections. Default +// is no limit. +func WithMaxConnections(mc uint64) Option { + return func(c *Command) { + c.conf.MaxConnections = mc + } +} + +// WithUserAgent sets additional user agents for Admin API tracking and should +// be a space separated list of additional user agents, e.g. +// cloud-sql-proxy-operator/0.0.1,other-agent/1.0.0 +func WithUserAgent(agent string) Option { + return func(c *Command) { + c.conf.OtherUserAgents = agent + } +} + +// WithAutoIP enables legacy behavior of v1 and will try to connect to first IP +// address returned by the SQL Admin API. In most cases, this flag should not +// be used. Prefer default of public IP or use --private-ip instead.` +func WithAutoIP() Option { + return func(c *Command) { + c.conf.AutoIP = true + } +} + +// WithQuietLogging configures the Proxy to log error messages only. +func WithQuietLogging() Option { + return func(c *Command) { + c.conf.Quiet = true + } +} + +// WithDebugLogging configures the Proxy to log debug level messages. +func WithDebugLogging() Option { + return func(c *Command) { + c.conf.DebugLogs = true + } +} + +// WithLazyRefresh configures the Proxy to refresh connection info on an +// as-needed basis when the cached copy has expired. +func WithLazyRefresh() Option { + return func(c *Command) { + c.conf.LazyRefresh = true + } +} + +// WithConnRefuseNotify configures the Proxy to call the provided function when +// a connection is refused. The notification function is run in a goroutine. +func WithConnRefuseNotify(n func()) Option { + return func(c *Command) { + c.connRefuseNotify = n + } +} diff --git a/cmd/options_test.go b/cmd/options_test.go new file mode 100644 index 000000000..c3dc718f6 --- /dev/null +++ b/cmd/options_test.go @@ -0,0 +1,225 @@ +// Copyright 2024 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" + "fmt" + "io" + "runtime" + "testing" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "github.com/spf13/cobra" +) + +type testDialer struct { + cloudsql.Dialer +} + +func TestCommandOptions(t *testing.T) { + logger := log.NewStdLogger(io.Discard, io.Discard) + dialer := &testDialer{} + tcs := []struct { + desc string + isValid func(*Command) error + option Option + skip bool + }{ + { + desc: "with logger", + isValid: func(c *Command) error { + if c.logger != logger { + return errors.New("loggers do not match") + } + return nil + }, + option: WithLogger(logger), + }, + { + desc: "with dialer", + isValid: func(c *Command) error { + if c.dialer != dialer { + return errors.New("dialers do not match") + } + return nil + }, + option: WithDialer(dialer), + }, + { + desc: "with FUSE dir", + isValid: func(c *Command) error { + if c.conf.FUSEDir != "somedir" { + return fmt.Errorf( + "want = %v, got = %v", "somedir", c.conf.FUSEDir, + ) + } + return nil + }, + option: WithFuseDir("somedir"), + // FUSE isn't available on GitHub macOS runners + // and FUSE isn't supported on Windows, so skip this test. + skip: runtime.GOOS == "darwin" || runtime.GOOS == "windows", + }, + { + desc: "with FUSE temp dir", + isValid: func(c *Command) error { + if c.conf.FUSETempDir != "somedir" { + return fmt.Errorf( + "want = %v, got = %v", "somedir", c.conf.FUSEDir, + ) + } + return nil + }, + option: WithFuseTempDir("somedir"), + // FUSE isn't available on GitHub macOS runners + // and FUSE isn't supported on Windows, so skip this test. + skip: runtime.GOOS == "darwin" || runtime.GOOS == "windows", + }, + { + desc: "with max connections", + isValid: func(c *Command) error { + if c.conf.MaxConnections != 1 { + return fmt.Errorf( + "want = %v, got = %v", 1, c.conf.MaxConnections, + ) + } + return nil + }, + option: WithMaxConnections(1), + }, + { + desc: "with user agent", + isValid: func(c *Command) error { + if c.conf.OtherUserAgents != "agents-go-here" { + return fmt.Errorf( + "want = %v, got = %v", + "agents-go-here", c.conf.OtherUserAgents, + ) + } + return nil + }, + option: WithUserAgent("agents-go-here"), + }, + { + desc: "with auto IP", + isValid: func(c *Command) error { + if !c.conf.AutoIP { + return errors.New("auto IP was false, but should be true") + } + return nil + }, + option: WithAutoIP(), + }, + { + desc: "with quiet logging", + isValid: func(c *Command) error { + if !c.conf.Quiet { + return errors.New("quiet was false, but should be true") + } + return nil + }, + option: WithQuietLogging(), + }, + { + desc: "with lazy refresh", + isValid: func(c *Command) error { + if !c.conf.LazyRefresh { + return errors.New( + "LazyRefresh was false, but should be true", + ) + } + return nil + }, + option: WithLazyRefresh(), + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + if tc.skip { + t.Skip("skipping unsupported test case") + } + got, err := invokeProxyWithOption(nil, tc.option) + if err != nil { + t.Fatal(err) + } + if err := tc.isValid(got); err != nil { + t.Errorf("option did not initialize command correctly: %v", err) + } + }) + } +} + +func TestCommandOptionsOverridesCLI(t *testing.T) { + tcs := []struct { + desc string + isValid func(*Command) error + option Option + args []string + }{ + { + desc: "with duplicate max connections", + isValid: func(c *Command) error { + if c.conf.MaxConnections != 10 { + return errors.New("max connections do not match") + } + return nil + }, + option: WithMaxConnections(10), + args: []string{"--max-connections", "20"}, + }, + { + desc: "with quiet logging", + isValid: func(c *Command) error { + if !c.conf.Quiet { + return errors.New("quiet was false, but should be true") + } + return nil + }, + option: WithQuietLogging(), + args: []string{"--quiet", "false"}, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + got, err := invokeProxyWithOption(tc.args, tc.option) + if err != nil { + t.Fatal(err) + } + if err := tc.isValid(got); err != nil { + t.Errorf("option did not initialize command correctly: %v", err) + } + }) + } +} + +func invokeProxyWithOption(args []string, o Option) (*Command, error) { + c := NewCommand(o) + // Keep the test output quiet + c.SilenceUsage = true + c.SilenceErrors = true + // Disable execute behavior + c.RunE = func(*cobra.Command, []string) error { + return nil + } + args = append(args, "test-project:us-central1:test-instance") + c.SetArgs(args) + + err := c.Execute() + + return c, err +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 000000000..ebc78ddde --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,1270 @@ +// 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. + +package cmd + +import ( + "context" + _ "embed" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/pprof" + "net/url" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "contrib.go.opencensus.io/exporter/prometheus" + "contrib.go.opencensus.io/exporter/stackdriver" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/healthcheck" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" + "github.com/coreos/go-systemd/v22/daemon" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.opencensus.io/trace" +) + +var ( + // versionString indicates the version of this library. + //go:embed version.txt + versionString string + // metadataString indicates additional build or distribution metadata. + metadataString string + userAgent string +) + +func init() { + versionString = semanticVersion() + userAgent = "cloud-sql-proxy/" + versionString +} + +// semanticVersion returns the version of the proxy including a compile-time +// metadata. +func semanticVersion() string { + v := strings.TrimSpace(versionString) + if metadataString != "" { + v += "+" + metadataString + } + return v +} + +// 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 + var terr *exitError + if errors.As(err, &terr) { + exit = terr.Code + } + os.Exit(exit) + } +} + +// Command represents an invocation of the Cloud SQL Auth Proxy. +type Command struct { + *cobra.Command + conf *proxy.Config + logger cloudsql.Logger + dialer cloudsql.Dialer + cleanup func() error + connRefuseNotify func() +} + +var longHelp = ` +Overview + + The Cloud SQL Auth Proxy is a utility for ensuring secure connections to + your Cloud SQL instances. It provides IAM authorization, allowing you to + control who can connect to your instance through IAM permissions, and TLS + 1.3 encryption, without having to manage certificates. + + NOTE: The Proxy does not configure the network. You MUST ensure the Proxy + can reach your Cloud SQL instance, either by deploying it in a VPC that has + access to your Private IP instance, or by configuring Public IP. + + For every provided instance connection name, the Proxy creates: + + - a socket that mimics a database running locally, and + - an encrypted connection using TLS 1.3 back to your Cloud SQL instance. + + The Proxy uses an ephemeral certificate to establish a secure connection to + your Cloud SQL instance. The Proxy will refresh those certificates on an + hourly basis. Existing client connections are unaffected by the refresh + cycle. + +Starting the Proxy + + To start the Proxy, you will need your instance connection name, which may + be found in the Cloud SQL instance overview page or by using gcloud with the + following command: + + gcloud sql instances describe INSTANCE --format='value(connectionName)' + + For example, if your instance connection name is + "my-project:us-central1:my-db-server", starting the Proxy will be: + + ./cloud-sql-proxy my-project:us-central1:my-db-server + + By default, the Proxy will determine the database engine and start a + listener on localhost using the default database engine's port, i.e., MySQL + is 3306, Postgres is 5432, SQL Server is 1433. If multiple instances are + specified which all use the same database engine, the first will be started + on the default database port and subsequent instances will be incremented + from there (e.g., 3306, 3307, 3308, etc). To disable this behavior (and + reduce startup time), use the --port flag. All subsequent listeners will + increment from the provided value. + + All socket listeners use the localhost network interface. To override this + behavior, use the --address flag. + +Instance Level Configuration + + The Proxy supports overriding configuration on an instance-level with an + optional query string syntax using the corresponding full flag name. The + query string takes the form of a URL query string and should be appended to + the INSTANCE_CONNECTION_NAME, e.g., + + 'my-project:us-central1:my-db-server?key1=value1&key2=value2' + + When using the optional query string syntax, quotes must wrap the instance + connection name and query string to prevent conflicts with the shell. For + example, to override the address and port for one instance but otherwise use + the default behavior, use: + + ./cloud-sql-proxy \ + my-project:us-central1:my-db-server \ + 'my-project:us-central1:my-other-server?address=0.0.0.0&port=7000' + + When necessary, you may specify the full path to a Unix socket. Set the + unix-socket-path query parameter to the absolute path of the Unix socket for + the database instance. The parent directory of the unix-socket-path must + exist when the Proxy starts or else socket creation will fail. For Postgres + instances, the Proxy will ensure that the last path element is + '.s.PGSQL.5432' appending it if necessary. For example, + + ./cloud-sql-proxy \ + 'my-project:us-central1:my-db-server?unix-socket-path=/path/to/socket' + +Health checks + + When enabling the --health-check flag, the Proxy will start an HTTP server + on localhost with three endpoints: + + - /startup: Returns 200 status when the Proxy has finished starting up. + Otherwise returns 503 status. + + - /readiness: Returns 200 status when the Proxy has started, has available + connections if max connections have been set with the --max-connections + flag, and when the Proxy can connect to all registered instances. Otherwise, + returns a 503 status. + + - /liveness: Always returns 200 status. If this endpoint is not responding, + the Proxy is in a bad state and should be restarted. + + To configure the address, use --http-address. To configure the port, use + --http-port. + +Service Account Impersonation + + The Proxy supports service account impersonation with the + --impersonate-service-account flag and matches gclouds flag. When enabled, + all API requests are made impersonating the supplied service account. The + IAM principal must have the iam.serviceAccounts.getAccessToken permission or + the role roles/iam.serviceAccounts.serviceAccountTokenCreator. + + For example: + + ./cloud-sql-proxy \ + --impersonate-service-account=impersonated@my-project.iam.gserviceaccount.com + my-project:us-central1:my-db-server + + In addition, the flag supports an impersonation delegation chain where the + value is a comma-separated list of service accounts. The first service + account in the list is the impersonation target. Each subsequent service + account is a delegate to the previous service account. When delegation is + used, each delegate must have the permissions named above on the service + account it is delegating to. + + For example: + + ./cloud-sql-proxy \ + --impersonate-service-account=SERVICE_ACCOUNT_1,SERVICE_ACCOUNT_2,SERVICE_ACCOUNT_3 + my-project:us-central1:my-db-server + + In this example, the environment's IAM principal impersonates + SERVICE_ACCOUNT_3 which impersonates SERVICE_ACCOUNT_2 which then + impersonates the target SERVICE_ACCOUNT_1. + +Configuration using environment variables + + Instead of using CLI flags, the Proxy may be configured using environment + variables. Each environment variable uses "CSQL_PROXY" as a prefix and is + the uppercase version of the flag using underscores as word delimiters. For + example, the --auto-iam-authn flag may be set with the environment variable + CSQL_PROXY_AUTO_IAM_AUTHN. An invocation of the Proxy using environment + variables would look like the following: + + CSQL_PROXY_AUTO_IAM_AUTHN=true \ + ./cloud-sql-proxy my-project:us-central1:my-db-server + + In addition to CLI flags, instance connection names may also be specified + with environment variables. If invoking the Proxy with only one instance + connection name, use CSQL_PROXY_INSTANCE_CONNECTION_NAME. For example: + + CSQL_PROXY_INSTANCE_CONNECTION_NAME=my-project:us-central1:my-db-server \ + ./cloud-sql-proxy + + If multiple instance connection names are used, add the index of the + instance connection name as a suffix. For example: + + CSQL_PROXY_INSTANCE_CONNECTION_NAME_0=my-project:us-central1:my-db-server \ + CSQL_PROXY_INSTANCE_CONNECTION_NAME_1=my-other-project:us-central1:my-other-server \ + ./cloud-sql-proxy + +Configuration using a configuration file + + Instead of using CLI flags, the Proxy may be configured using a configuration + file. The configuration file is a TOML, YAML or JSON file with the same keys + as the environment variables. The configuration file is specified with the + --config-file flag. An invocation of the Proxy using a configuration file + would look like the following: + + ./cloud-sql-proxy --config-file=config.toml + + The configuration file may look like the following: + + instance-connection-name = "my-project:us-central1:my-server-instance" + auto-iam-authn = true + + If multiple instance connection names are used, add the index of the + instance connection name as a suffix. For example: + + instance-connection-name-0 = "my-project:us-central1:my-db-server" + instance-connection-name-1 = "my-other-project:us-central1:my-other-server" + + The configuration file may also contain the same keys as the environment + variables and flags. For example: + + auto-iam-authn = true + debug = true + max-connections = 5 + +Localhost Admin Server + + The Proxy includes support for an admin server on localhost. By default, + the admin server is not enabled. To enable the server, pass the --debug or + --quitquitquit flag. This will start the server on localhost at port 9091. + To change the port, use the --admin-port flag. + + When --debug is set, the admin server enables Go's profiler available at + /debug/pprof/. + + See the documentation on pprof for details on how to use the + profiler at https://pkg.go.dev/net/http/pprof. + + When --quitquitquit is set, the admin server adds an endpoint at + /quitquitquit. The admin server exits gracefully when it receives a GET or POST + request at /quitquitquit. + +Debug logging + + On occasion, it can help to enable debug logging which will report on + internal certificate refresh operations. To enable debug logging, use: + + ./cloud-sql-proxy --debug-logs + +Waiting for Startup + + See the wait subcommand's help for details. + +(*) indicates a flag that may be used as a query parameter + +Third Party Licenses + + To view all licenses for third party dependencies used within this + distribution please see: + + https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1/third_party/licenses.tar.gz {x-release-please-version} +` + +var waitHelp = ` +Waiting for Proxy Startup + + Sometimes it is necessary to wait for the Proxy to start. + + To help ensure the Proxy is up and ready, the Proxy includes a wait + subcommand with an optional --max flag to set the maximum time to wait. + The wait command uses a separate Proxy's startup endpoint to determine + if the other Proxy process is ready. + + Invoke the wait command, like this: + + # waits for another Proxy process' startup endpoint to respond + ./cloud-sql-proxy wait + +Configuration + + By default, the Proxy will wait up to the maximum time for the startup + endpoint to respond. The wait command requires that the Proxy be started in + another process with the HTTP health check or Prometheus enabled. If an + alternate health check port or address is used, as in: + + ./cloud-sql-proxy \ + --http-address 0.0.0.0 \ + --http-port 9191 \ + --health-check + + Then the wait command must also be told to use the same custom values: + + ./cloud-sql-proxy wait \ + --http-address 0.0.0.0 \ + --http-port 9191 + + By default the wait command will wait 30 seconds. To alter this value, + use: + + ./cloud-sql-proxy wait --max 10s +` + +var shutdownHelp = ` +Shutting Down the Proxy + + The shutdown command signals a running Proxy process to gracefully shut + down. This is useful for scripting and for Kubernetes environments. + + The shutdown command requires that the Proxy be started in another process + with the admin server enabled. For example: + + ./cloud-sql-proxy --quitquitquit + + Invoke the shutdown command like this: + + # signals another Proxy process to shut down + ./cloud-sql-proxy shutdown + +Configuration + + If the running Proxy is configured with a non-default admin port, the + shutdown command must also be told to use the same custom value: + + ./cloud-sql-proxy shutdown --admin-port 9192 +` + +const ( + waitMaxFlag = "max" + adminPortFlag = "admin-port" + httpAddressFlag = "http-address" + httpPortFlag = "http-port" +) + +func runWaitCmd(c *cobra.Command, _ []string) error { + a, _ := c.Flags().GetString(httpAddressFlag) + p, _ := c.Flags().GetString(httpPortFlag) + addr := fmt.Sprintf("http://%v:%v/startup", a, p) + + wait, err := c.Flags().GetDuration(waitMaxFlag) + if err != nil { + // This error should always be nil. If the error occurs, it means the + // wait flag name has changed where it was registered. + return err + } + c.SilenceUsage = true + + t := time.After(wait) + for { + select { + case <-t: + return errors.New("command failed to complete successfully") + default: + resp, err := http.Get(addr) + if err != nil || resp.StatusCode != http.StatusOK { + time.Sleep(time.Second) + break + } + return nil + } + } +} + +func runShutdownCmd(c *cobra.Command, _ []string) error { + p, _ := c.Flags().GetString(adminPortFlag) + addr := fmt.Sprintf("http://127.0.0.1:%v/quitquitquit", p) + c.SilenceUsage = true + + req, err := http.NewRequestWithContext(c.Context(), "POST", addr, nil) + if err != nil { + return fmt.Errorf("failed to create shutdown request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send shutdown request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("shutdown request failed: status code %v, %v", resp.StatusCode, resp.Status) + } + + return nil +} + +const envPrefix = "CSQL_PROXY" + +// NewCommand returns a Command object representing an invocation of the proxy. +func NewCommand(opts ...Option) *Command { + rootCmd := &cobra.Command{ + Use: "cloud-sql-proxy INSTANCE_CONNECTION_NAME...", + Version: versionString, + Short: "cloud-sql-proxy authorizes and encrypts connections to Cloud SQL.", + //remove the inline annotation required by release-please to update version. + Long: strings.ReplaceAll(longHelp, "{x-release-please-version}", ""), + } + + logger := log.NewStdLogger(os.Stdout, os.Stderr) + c := &Command{ + Command: rootCmd, + logger: logger, + cleanup: func() error { return nil }, + conf: &proxy.Config{ + UserAgent: userAgent, + }, + } + var waitCmd = &cobra.Command{ + Use: "wait", + Short: "Wait for another Proxy process to start", + Long: waitHelp, + RunE: runWaitCmd, + } + waitFlags := waitCmd.Flags() + waitFlags.DurationP( + waitMaxFlag, "m", + 30*time.Second, + "maximum amount of time to wait for startup", + ) + rootCmd.AddCommand(waitCmd) + + var shutdownCmd = &cobra.Command{ + Use: "shutdown", + Short: "Signal a running Proxy process to shut down", + Long: shutdownHelp, + RunE: runShutdownCmd, + } + shutdownFlags := shutdownCmd.Flags() + shutdownFlags.String( + adminPortFlag, + "9091", + "port for the admin server", + ) + rootCmd.AddCommand(shutdownCmd) + + rootCmd.Args = func(_ *cobra.Command, args []string) error { + // Load the configuration file before running the command. This should + // ensure that the configuration is loaded in the correct order: + // + // flags > environment variables > configuration files + // + // See https://github.com/carolynvs/stingoftheviper for more info + return loadConfig(c, args, opts) + } + rootCmd.RunE = func(*cobra.Command, []string) error { return runSignalWrapper(c) } + + // Flags that apply only to the root command + localFlags := rootCmd.Flags() + // Flags that apply to all sub-commands + globalFlags := rootCmd.PersistentFlags() + + localFlags.BoolP("help", "h", false, "Display help information for cloud-sql-proxy") + localFlags.BoolP("version", "v", false, "Print the cloud-sql-proxy version") + + localFlags.StringVar(&c.conf.Filepath, "config-file", c.conf.Filepath, + "Path to a TOML file containing configuration options.") + localFlags.StringVar(&c.conf.OtherUserAgents, "user-agent", "", + "Space separated list of additional user agents, e.g. cloud-sql-proxy-operator/0.0.1") + localFlags.StringVarP(&c.conf.Token, "token", "t", "", + "Use bearer token as a source of IAM credentials.") + localFlags.StringVar(&c.conf.LoginToken, "login-token", "", + "Use bearer token as a database password (used with token and auto-iam-authn only)") + localFlags.StringVarP(&c.conf.CredentialsFile, "credentials-file", "c", "", + "Use service account key file as a source of IAM credentials.") + localFlags.StringVarP(&c.conf.CredentialsJSON, "json-credentials", "j", "", + "Use service account key JSON as a source of IAM credentials.") + localFlags.BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false, + `Use gclouds user credentials as a source of IAM credentials. +NOTE: this flag is a legacy feature and generally should not be used. +Instead prefer Application Default Credentials +(enabled with: gcloud auth application-default login) which +the Proxy will then pick-up automatically.`) + localFlags.BoolVarP(&c.conf.StructuredLogs, "structured-logs", "l", false, + "Enable structured logging with LogEntry format") + localFlags.BoolVar(&c.conf.DebugLogs, "debug-logs", false, + "Enable debug logging") + localFlags.Uint64Var(&c.conf.MaxConnections, "max-connections", 0, + "Limit the number of connections. Default is no limit.") + localFlags.DurationVar(&c.conf.WaitBeforeClose, "min-sigterm-delay", 0, + "The number of seconds to accept new connections after receiving a TERM signal.") + localFlags.DurationVar(&c.conf.WaitOnClose, "max-sigterm-delay", 0, + "Maximum number of seconds to wait for connections to close after receiving a TERM signal.") + localFlags.StringVar(&c.conf.TelemetryProject, "telemetry-project", "", + "Enable Cloud Monitoring and Cloud Trace with the provided project ID.") + localFlags.BoolVar(&c.conf.DisableTraces, "disable-traces", false, + "Disable Cloud Trace integration (used with --telemetry-project)") + localFlags.IntVar(&c.conf.TelemetryTracingSampleRate, "telemetry-sample-rate", 10_000, + "Set the Cloud Trace sample rate. A smaller number means more traces.") + localFlags.BoolVar(&c.conf.DisableMetrics, "disable-metrics", false, + "Disable Cloud Monitoring integration (used with --telemetry-project)") + localFlags.StringVar(&c.conf.TelemetryPrefix, "telemetry-prefix", "", + "Prefix for Cloud Monitoring metrics.") + localFlags.BoolVar(&c.conf.ExitZeroOnSigterm, "exit-zero-on-sigterm", false, + "Exit with 0 exit code when Sigterm received (default is 143)") + localFlags.BoolVar(&c.conf.Prometheus, "prometheus", false, + "Enable Prometheus HTTP endpoint /metrics on localhost") + localFlags.StringVar(&c.conf.PrometheusNamespace, "prometheus-namespace", "", + "Use the provided Prometheus namespace for metrics") + globalFlags.StringVar(&c.conf.HTTPAddress, httpAddressFlag, "localhost", + "Address for Prometheus and health check server") + globalFlags.StringVar(&c.conf.HTTPPort, httpPortFlag, "9090", + "Port for Prometheus and health check server") + localFlags.BoolVar(&c.conf.Debug, "debug", false, + "Enable pprof on the localhost admin server") + localFlags.BoolVar(&c.conf.QuitQuitQuit, "quitquitquit", false, + "Enable quitquitquit endpoint on the localhost admin server") + localFlags.StringVar(&c.conf.AdminPort, adminPortFlag, "9091", + "Port for localhost-only admin server") + localFlags.BoolVar(&c.conf.HealthCheck, "health-check", false, + "Enables health check endpoints /startup, /liveness, and /readiness on localhost.") + localFlags.StringVar(&c.conf.APIEndpointURL, "sqladmin-api-endpoint", "", + "API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com)") + localFlags.StringVar(&c.conf.UniverseDomain, "universe-domain", "", + "Universe Domain for non-GDU environments. (default: googleapis.com)") + localFlags.StringVar(&c.conf.QuotaProject, "quota-project", "", + `Specifies the project to use for Cloud SQL Admin API quota tracking. +The IAM principal must have the "serviceusage.services.use" permission +for the given project. See https://cloud.google.com/service-usage/docs/overview and +https://cloud.google.com/storage/docs/requester-pays`) + localFlags.StringVar(&c.conf.FUSEDir, "fuse", "", + "Mount a directory at the path using FUSE to access Cloud SQL instances.") + localFlags.StringVar(&c.conf.FUSETempDir, "fuse-tmp-dir", + filepath.Join(os.TempDir(), "csql-tmp"), + "Temp dir for Unix sockets created with FUSE") + localFlags.StringVar(&c.conf.ImpersonationChain, "impersonate-service-account", "", + `Comma separated list of service accounts to impersonate. Last value +is the target account.`) + localFlags.BoolVar(&c.conf.Quiet, "quiet", false, "Log error messages only") + localFlags.BoolVar(&c.conf.AutoIP, "auto-ip", false, + `Supports legacy behavior of v1 and will try to connect to first IP +address returned by the SQL Admin API. In most cases, this flag should not be used. +Prefer default of public IP or use --private-ip instead.`) + localFlags.BoolVar(&c.conf.LazyRefresh, "lazy-refresh", false, + `Configure a lazy refresh where connection info is retrieved only if +the cached copy has expired. Use this setting in environments where the +CPU may be throttled and a background refresh cannot run reliably +(e.g., Cloud Run)`, + ) + + localFlags.BoolVar(&c.conf.RunConnectionTest, "run-connection-test", false, `Runs a connection test +against all specified instances. If an instance is unreachable, the Proxy exits with a failure +status code.`) + + localFlags.BoolVar(&c.conf.SkipFailedInstanceConfig, "skip-failed-instance-config", false, + `If set, the Proxy will skip any instances that are invalid/unreachable ( +only applicable to Unix sockets)`) + + // Global and per instance flags + localFlags.StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1", + "(*) Address to bind Cloud SQL instance listeners.") + localFlags.IntVarP(&c.conf.Port, "port", "p", 0, + "(*) Initial port for listeners. Subsequent listeners increment from this value.") + localFlags.StringVarP(&c.conf.UnixSocket, "unix-socket", "u", "", + `(*) Enables Unix sockets for all listeners with the provided directory.`) + localFlags.BoolVarP(&c.conf.IAMAuthN, "auto-iam-authn", "i", false, + "(*) Enables Automatic IAM Authentication for all instances") + localFlags.BoolVar(&c.conf.PrivateIP, "private-ip", false, + "(*) Connect to the private ip address for all instances") + localFlags.BoolVar(&c.conf.PSC, "psc", false, + "(*) Connect to the PSC endpoint for all instances") + + return c +} + +func loadConfig(c *Command, args []string, opts []Option) error { + v, err := initViper(c) + if err != nil { + return err + } + + c.Flags().VisitAll(func(f *pflag.Flag) { + // Override any unset flags with Viper values to use the pflags + // object as a single source of truth. + if !f.Changed && v.IsSet(f.Name) { + val := v.Get(f.Name) + _ = c.Flags().Set(f.Name, fmt.Sprintf("%v", val)) + } + }) + + // If args is not already populated, try to read from the environment. + if len(args) == 0 { + args = instanceFromEnv(args) + } + + // If no environment args are present, try to read from the config file. + if len(args) == 0 { + args = instanceFromConfigFile(v) + } + + for _, o := range opts { + o(c) + } + + // Handle logger separately from config + if c.conf.StructuredLogs { + c.logger = log.NewStructuredLogger(c.conf.Quiet) + } + + if c.conf.Quiet { + c.logger = log.NewStdLogger(io.Discard, os.Stderr) + } + + err = parseConfig(c, c.conf, args) + if err != nil { + return err + } + + // The arguments are parsed. Usage is no longer needed. + c.SilenceUsage = true + + // Errors will be handled by logging from here on. + c.SilenceErrors = true + + return nil +} + +func initViper(c *Command) (*viper.Viper, error) { + v := viper.New() + + if c.conf.Filepath != "" { + // Setup Viper configuration file. Viper will attempt to load + // configuration from the specified file if it exists. Otherwise, Viper + // will source all configuration from flags and then environment + // variables. + ext := filepath.Ext(c.conf.Filepath) + + badExtErr := newBadCommandError( + fmt.Sprintf("config file %v should have extension of "+ + "toml, yaml, or json", c.conf.Filepath, + )) + + if ext == "" { + return nil, badExtErr + } + + if ext != ".toml" && ext != ".yaml" && ext != ".yml" && ext != ".json" { + return nil, badExtErr + } + + conf := filepath.Base(c.conf.Filepath) + noExt := strings.ReplaceAll(conf, ext, "") + // argument must be the name of config file without extension + v.SetConfigName(noExt) + v.AddConfigPath(filepath.Dir(c.conf.Filepath)) + + // Attempt to load configuration from a file. If no file is found, + // assume configuration is provided by flags or environment variables. + if err := v.ReadInConfig(); err != nil { + // If the error is a ConfigFileNotFoundError, then ignore it. + // Otherwise, report the error to the user. + var cErr viper.ConfigFileNotFoundError + if !errors.As(err, &cErr) { + return nil, newBadCommandError(fmt.Sprintf( + "failed to load configuration from %v: %v", + c.conf.Filepath, err, + )) + } + } + } + + v.SetEnvPrefix(envPrefix) + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.AutomaticEnv() + + return v, nil +} + +func instanceFromEnv(args []string) []string { + // This supports naming the first instance first with: + // INSTANCE_CONNECTION_NAME + // or if that's not defined, with: + // INSTANCE_CONNECTION_NAME_0 + inst := os.Getenv(fmt.Sprintf("%s_INSTANCE_CONNECTION_NAME", envPrefix)) + if inst == "" { + inst = os.Getenv(fmt.Sprintf("%s_INSTANCE_CONNECTION_NAME_0", envPrefix)) + if inst == "" { + return nil + } + } + args = append(args, inst) + + i := 1 + for { + instN := os.Getenv(fmt.Sprintf("%s_INSTANCE_CONNECTION_NAME_%d", envPrefix, i)) + // if the next instance connection name is not defined, stop checking + // environment variables. + if instN == "" { + break + } + args = append(args, instN) + i++ + } + return args +} + +func instanceFromConfigFile(v *viper.Viper) []string { + var args []string + inst := v.GetString("instance-connection-name") + + if inst == "" { + inst = v.GetString("instance-connection-name-0") + if inst == "" { + return nil + } + } + args = append(args, inst) + + i := 1 + for { + instN := v.GetString(fmt.Sprintf("instance-connection-name-%d", i)) + // if the next instance connection name is not defined, stop checking + // environment variables. + if instN == "" { + break + } + args = append(args, instN) + i++ + } + + return args +} + +func userHasSetLocal(cmd *Command, f string) bool { + return cmd.LocalFlags().Lookup(f).Changed +} + +func userHasSetGlobal(cmd *Command, f string) bool { + return cmd.PersistentFlags().Lookup(f).Changed +} + +func parseConfig(cmd *Command, conf *proxy.Config, args []string) error { + // If no instance connection names were provided AND FUSE isn't enabled, + // error. + if len(args) == 0 && conf.FUSEDir == "" { + return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)") + } + + if conf.FUSEDir != "" { + if conf.RunConnectionTest { + return newBadCommandError("cannot run connection tests in FUSE mode") + } + + if err := proxy.SupportsFUSE(); err != nil { + return newBadCommandError( + fmt.Sprintf("--fuse is not supported: %v", err), + ) + } + } + + if len(args) == 0 && conf.FUSEDir == "" && conf.FUSETempDir != "" { + return newBadCommandError("cannot specify --fuse-tmp-dir without --fuse") + } + + if userHasSetLocal(cmd, "address") && userHasSetLocal(cmd, "unix-socket") { + return newBadCommandError("cannot specify --unix-socket and --address together") + } + if userHasSetLocal(cmd, "port") && userHasSetLocal(cmd, "unix-socket") { + return newBadCommandError("cannot specify --unix-socket and --port together") + } + if ip := net.ParseIP(conf.Addr); ip == nil { + return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr)) + } + if userHasSetLocal(cmd, "private-ip") && userHasSetLocal(cmd, "auto-ip") { + return newBadCommandError("cannot specify --private-ip and --auto-ip together") + } + + // If more than one IP type is set, error. + if conf.PrivateIP && conf.PSC { + return newBadCommandError("cannot specify --private-ip and --psc flags at the same time") + } + + // If more than one auth method is set, error. + if conf.Token != "" && conf.CredentialsFile != "" { + return newBadCommandError("cannot specify --token and --credentials-file flags at the same time") + } + if conf.Token != "" && conf.GcloudAuth { + return newBadCommandError("cannot specify --token and --gcloud-auth flags at the same time") + } + if conf.CredentialsFile != "" && conf.GcloudAuth { + return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time") + } + if conf.CredentialsJSON != "" && conf.Token != "" { + return newBadCommandError("cannot specify --json-credentials and --token flags at the same time") + } + if conf.CredentialsJSON != "" && conf.CredentialsFile != "" { + return newBadCommandError("cannot specify --json-credentials and --credentials-file flags at the same time") + } + if conf.CredentialsJSON != "" && conf.GcloudAuth { + return newBadCommandError("cannot specify --json-credentials and --gcloud-auth flags at the same time") + } + + // When using token with auto-iam-authn, login-token must also be set. + // All three are required together. + if conf.IAMAuthN && conf.Token != "" && conf.LoginToken == "" { + return newBadCommandError("cannot specify --auto-iam-authn and --token without --login-token") + } + if conf.IAMAuthN && conf.GcloudAuth { + return newBadCommandError(`cannot use --auto-iam-authn with --gcloud-auth. +Instead use Application Default Credentials (enabled with: gcloud auth application-default login) +and re-try with just --auto-iam-authn`) + } + if conf.LoginToken != "" && (conf.Token == "" || !conf.IAMAuthN) { + return newBadCommandError("cannot specify --login-token without --token and --auto-iam-authn") + } + + if userHasSetGlobal(cmd, "http-port") && !userHasSetLocal(cmd, "prometheus") && !userHasSetLocal(cmd, "health-check") { + cmd.logger.Infof("Ignoring --http-port because --prometheus or --health-check was not set") + } + + if !userHasSetLocal(cmd, "telemetry-project") && userHasSetLocal(cmd, "telemetry-prefix") { + cmd.logger.Infof("Ignoring --telementry-prefix because --telemetry-project was not set") + } + if !userHasSetLocal(cmd, "telemetry-project") && userHasSetLocal(cmd, "disable-metrics") { + cmd.logger.Infof("Ignoring --disable-metrics because --telemetry-project was not set") + } + if !userHasSetLocal(cmd, "telemetry-project") && userHasSetLocal(cmd, "disable-traces") { + cmd.logger.Infof("Ignoring --disable-traces because --telemetry-project was not set") + } + + if userHasSetLocal(cmd, "user-agent") { + userAgent += " " + cmd.conf.OtherUserAgents + conf.UserAgent = userAgent + } + + if userHasSetLocal(cmd, "sqladmin-api-endpoint") && userHasSetLocal(cmd, "universe-domain") { + return newBadCommandError("cannot specify --sqladmin-api-endpoint and --universe-domain at the same time") + } + if userHasSetLocal(cmd, "sqladmin-api-endpoint") && conf.APIEndpointURL != "" { + _, err := url.Parse(conf.APIEndpointURL) + if err != nil { + return newBadCommandError(fmt.Sprintf( + "the value provided for --sqladmin-api-endpoint is not a valid URL, %v", + conf.APIEndpointURL, + )) + } + + // add a trailing '/' if omitted + if !strings.HasSuffix(conf.APIEndpointURL, "/") { + conf.APIEndpointURL = conf.APIEndpointURL + "/" + } + } + + 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])) + } + + a, aok := q["address"] + p, pok := q["port"] + u, uok := q["unix-socket"] + up, upok := q["unix-socket-path"] + + if aok && uok { + return newBadCommandError("cannot specify both address and unix-socket query params") + } + if pok && uok { + return newBadCommandError("cannot specify both port and unix-socket query params") + } + if aok && upok { + return newBadCommandError("cannot specify both address and unix-socket-path query params") + } + if pok && upok { + return newBadCommandError("cannot specify both port and unix-socket-path query params") + } + if uok && upok { + return newBadCommandError("cannot specify both unix-socket-path and unix-socket query params") + } + + if aok { + 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 pok { + 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 + } + + if uok { + if len(u) != 1 { + return newBadCommandError(fmt.Sprintf("unix query param should be only one value: %q", a)) + } + ic.UnixSocket = u[0] + } + + if upok { + if len(up) != 1 { + return newBadCommandError(fmt.Sprintf("unix-socket-path query param should be only one value: %q", a)) + } + ic.UnixSocketPath = up[0] + } + + ic.IAMAuthN, err = parseBoolOpt(q, "auto-iam-authn") + if err != nil { + return err + } + + ic.PrivateIP, err = parseBoolOpt(q, "private-ip") + if err != nil { + return err + } + if ic.PrivateIP != nil && *ic.PrivateIP && conf.AutoIP { + return newBadCommandError("cannot use --auto-ip with private-ip") + } + + ic.PSC, err = parseBoolOpt(q, "psc") + if err != nil { + return err + } + + if ic.PrivateIP != nil && ic.PSC != nil { + return newBadCommandError("cannot specify both private-ip and psc query params") + } + + } + ics = append(ics, ic) + } + + conf.Instances = ics + return nil +} + +// parseBoolOpt parses a boolean option from the query string, returning +// +// true if the value is "t" or "true" case-insensitive +// false if the value is "f" or "false" case-insensitive +func parseBoolOpt(q url.Values, name string) (*bool, error) { + v, ok := q[name] + if !ok { + return nil, nil + } + + if len(v) != 1 { + return nil, newBadCommandError(fmt.Sprintf("%v param should be only one value: %q", name, v)) + } + + switch strings.ToLower(v[0]) { + case "true", "t", "": + enable := true + return &enable, nil + case "false", "f": + disable := false + return &disable, nil + default: + // value is not recognized + return nil, newBadCommandError( + fmt.Sprintf("%v query param should be true or false, got: %q", + name, v[0], + )) + } + +} + +// runSignalWrapper watches for SIGTERM and SIGINT and interupts execution if necessary. +func runSignalWrapper(cmd *Command) (err error) { + defer func() { _ = cmd.cleanup() }() + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + // Configure collectors before the proxy has started to ensure we are + // collecting metrics before *ANY* Cloud SQL Admin API calls are made. + enableMetrics := !cmd.conf.DisableMetrics + enableTraces := !cmd.conf.DisableTraces + if cmd.conf.TelemetryProject != "" && (enableMetrics || enableTraces) { + sd, err := stackdriver.NewExporter(stackdriver.Options{ + ProjectID: cmd.conf.TelemetryProject, + MetricPrefix: cmd.conf.TelemetryPrefix, + }) + if err != nil { + return err + } + if enableMetrics { + err = sd.StartMetricsExporter() + if err != nil { + return err + } + } + if enableTraces { + s := trace.ProbabilitySampler(1 / float64(cmd.conf.TelemetryTracingSampleRate)) + trace.ApplyConfig(trace.Config{DefaultSampler: s}) + trace.RegisterExporter(sd) + } + defer func() { + sd.Flush() + sd.StopMetricsExporter() + }() + } + + 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 <-ctx.Done(): + // this should only happen when the context supplied in tests in canceled + s = syscall.SIGINT + } + switch s { + case syscall.SIGINT: + cmd.logger.Debugf("Sending SIGINT signal for proxy to shutdown.") + shutdownCh <- errSigInt + case syscall.SIGTERM: + cmd.logger.Debugf("Sending SIGTERM signal for proxy to shutdown.") + if cmd.conf.ExitZeroOnSigterm { + shutdownCh <- errSigTermZero + } else { + 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) + p, err := proxy.NewClient(ctx, cmd.dialer, cmd.logger, cmd.conf, cmd.connRefuseNotify) + if err != nil { + cmd.logger.Debugf("Error starting proxy: %v", err) + 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: + cmd.logger.Errorf("The proxy has encountered a terminal error: %v", err) + // If running under systemd with Type=notify, it will send a message to the + // service manager that a failure occurred, and it is terminating. + go func() { + if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { + cmd.logger.Errorf("Failed to notify systemd of termination: %v", err) + } + }() + return err + case p = <-startCh: + cmd.logger.Infof("The proxy has started successfully and is ready for new connections!") + // If running under systemd with Type=notify, it will send a message to the + // service manager that it is ready to handle connections now. + go func() { + if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + cmd.logger.Errorf("Failed to notify systemd of readiness: %v", err) + } + }() + } + defer func() { + if cErr := p.Close(); cErr != nil { + cmd.logger.Errorf("error during shutdown: %v", cErr) + // Capture error from close to propagate it to the caller. + err = cErr + } + }() + + var ( + needsHTTPServer bool + mux = http.NewServeMux() + notifyStarted = func() {} + notifyStopped = func() {} + ) + + if cmd.conf.Prometheus { + needsHTTPServer = true + e, err := prometheus.NewExporter(prometheus.Options{ + Namespace: cmd.conf.PrometheusNamespace, + }) + if err != nil { + return err + } + mux.Handle("/metrics", e) + } + + if cmd.conf.HealthCheck { + needsHTTPServer = true + cmd.logger.Infof("Starting health check server at %s", + net.JoinHostPort(cmd.conf.HTTPAddress, cmd.conf.HTTPPort)) + hc := healthcheck.NewCheck(p, cmd.logger) + mux.HandleFunc("/startup", hc.HandleStartup) + mux.HandleFunc("/readiness", hc.HandleReadiness) + mux.HandleFunc("/liveness", hc.HandleLiveness) + notifyStarted = hc.NotifyStarted + notifyStopped = hc.NotifyStopped + } + defer notifyStopped() + // Start the HTTP server if anything requiring HTTP is specified. + if needsHTTPServer { + go startHTTPServer( + ctx, + cmd.logger, + net.JoinHostPort(cmd.conf.HTTPAddress, cmd.conf.HTTPPort), + mux, + shutdownCh, + ) + } + + var ( + needsAdminServer bool + m = http.NewServeMux() + ) + if cmd.conf.QuitQuitQuit { + needsAdminServer = true + cmd.logger.Infof("Enabling quitquitquit endpoint at localhost:%v", cmd.conf.AdminPort) + // quitquitquit allows for shutdown on localhost only. + var quitOnce sync.Once + m.HandleFunc("/quitquitquit", quitquitquit(&quitOnce, shutdownCh)) + } + if cmd.conf.Debug { + needsAdminServer = true + cmd.logger.Infof("Enabling pprof endpoints at localhost:%v", cmd.conf.AdminPort) + // pprof standard endpoints + m.HandleFunc("/debug/pprof/", pprof.Index) + m.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + m.HandleFunc("/debug/pprof/profile", pprof.Profile) + m.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + m.HandleFunc("/debug/pprof/trace", pprof.Trace) + } + if needsAdminServer { + go startHTTPServer( + ctx, + cmd.logger, + net.JoinHostPort("localhost", cmd.conf.AdminPort), + m, + shutdownCh, + ) + } + + go func() { + err := p.Serve(ctx, notifyStarted) + cmd.logger.Debugf("proxy server error: %v", err) + shutdownCh <- err + }() + + err = <-shutdownCh + switch { + case errors.Is(err, errSigInt): + cmd.logger.Infof("SIGINT signal received. Shutting down...") + time.Sleep(cmd.conf.WaitBeforeClose) + case errors.Is(err, errSigTerm): + cmd.logger.Infof("SIGTERM signal received. Shutting down...") + time.Sleep(cmd.conf.WaitBeforeClose) + case errors.Is(err, errSigTermZero): + cmd.logger.Infof("SIGTERM signal received. Shutting down...") + time.Sleep(cmd.conf.WaitBeforeClose) + case errors.Is(err, errQuitQuitQuit): + cmd.logger.Infof("/quitquitquit received request. Shutting down...") + time.Sleep(cmd.conf.WaitBeforeClose) + default: + cmd.logger.Errorf("The proxy has encountered a terminal error: %v", err) + } + return err +} + +func quitquitquit(quitOnce *sync.Once, shutdownCh chan<- error) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost && req.Method != http.MethodGet { + rw.WriteHeader(400) + return + } + quitOnce.Do(func() { + select { + case shutdownCh <- errQuitQuitQuit: + default: + // The write attempt to shutdownCh failed and + // the proxy is already exiting. + } + }) + } +} + +func startHTTPServer(ctx context.Context, l cloudsql.Logger, addr string, mux *http.ServeMux, shutdownCh chan<- error) { + server := &http.Server{ + Addr: addr, + Handler: mux, + } + // Start the HTTP server. + go func() { + err := server.ListenAndServe() + if errors.Is(err, http.ErrServerClosed) { + return + } + if err != nil { + shutdownCh <- fmt.Errorf("failed to start HTTP server: %v", err) + } + }() + // Handle shutdown of the HTTP server gracefully. + <-ctx.Done() + // Give the HTTP server a second to shut down cleanly. + ctx2, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if err := server.Shutdown(ctx2); err != nil { + l.Errorf("failed to shutdown HTTP server: %v\n", err) + } +} diff --git a/cmd/root_linux_test.go b/cmd/root_linux_test.go new file mode 100644 index 000000000..121c826ed --- /dev/null +++ b/cmd/root_linux_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 cmd + +import ( + "context" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/coreos/go-systemd/v22/daemon" + "github.com/spf13/cobra" +) + +func TestNewCommandArgumentsOnLinux(t *testing.T) { + defaultTmp := filepath.Join(os.TempDir(), "csql-tmp") + tcs := []struct { + desc string + args []string + wantDir string + wantTempDir string + }{ + { + desc: "using the fuse flag", + args: []string{"--fuse", "/cloudsql"}, + wantDir: "/cloudsql", + wantTempDir: defaultTmp, + }, + { + desc: "using the fuse temporary directory flag", + args: []string{"--fuse", "/cloudsql", "--fuse-tmp-dir", "/mycooldir"}, + wantDir: "/cloudsql", + wantTempDir: "/mycooldir", + }, + } + + 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, want := c.conf.FUSEDir, tc.wantDir; got != want { + t.Fatalf("FUSEDir: want = %v, got = %v", want, got) + } + + if got, want := c.conf.FUSETempDir, tc.wantTempDir; got != want { + t.Fatalf("FUSEDir: want = %v, got = %v", want, got) + } + }) + } +} + +func TestSdNotifyOnLinux(t *testing.T) { + tcs := []struct { + desc string + proxyMustFail bool + notifyState string + }{ + { + desc: "System with systemd Type=notify and proxy started successfully", + proxyMustFail: false, + notifyState: daemon.SdNotifyReady, + }, + { + desc: "System with systemd Type=notify and proxy failed to start", + proxyMustFail: true, + notifyState: daemon.SdNotifyStopping, + }, + } + + // Create a temp dir for the socket file. + testDir, err := os.MkdirTemp("/tmp/", "test-") + if err != nil { + t.Fatalf("Fail to create the temp dir: %v", err) + } + defer os.RemoveAll(testDir) + + //Set up the socket stream to listen for notifications. + socketAddr := filepath.Join(testDir, "notify-socket.sock") + conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: socketAddr, Net: "unixgram"}) + if err != nil { + t.Fatalf("net.ListenUnixgram error: %v", err) + } + + // To simulate systemd behavior with Type=notify, set NOTIFY_SOCKET + // to the name of the socket that listens for notifications. + os.Setenv("NOTIFY_SOCKET", socketAddr) + defer os.Unsetenv("NOTIFY_SOCKET") + + s := &spyDialer{} + c := NewCommand(WithDialer(s)) + // Keep the test output quiet + c.SilenceUsage = false + c.SilenceErrors = false + c.SetArgs([]string{"my-project:my-region:my-instance"}) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + + if tc.proxyMustFail { + c.conf.FUSEDir = "invalid" + } + + go c.ExecuteContext(ctx) + + stateReceived := make([]byte, 4096) + length, _, err := conn.ReadFromUnix(stateReceived) + if err != nil { + t.Fatalf("conn.ReadFromUnix error: %s\n", err) + } + if string(stateReceived[0:length]) != tc.notifyState { + t.Fatalf("Expected Notify State %v, got %v", tc.notifyState, string(stateReceived)) + } + + }) + } +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 000000000..4a4be698c --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,1461 @@ +// 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" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" + "github.com/google/go-cmp/cmp" + "github.com/spf13/cobra" +) + +func withDefaults(c *proxy.Config) *proxy.Config { + if c.UserAgent == "" { + c.UserAgent = userAgent + } + if c.Addr == "" { + c.Addr = "127.0.0.1" + } + if c.FUSEDir == "" { + if c.Instances == nil { + c.Instances = []proxy.InstanceConnConfig{{}} + } + if i := &c.Instances[0]; i.Name == "" { + i.Name = "proj:region:inst" + } + } + if c.FUSETempDir == "" { + c.FUSETempDir = filepath.Join(os.TempDir(), "csql-tmp") + } + if c.HTTPAddress == "" { + c.HTTPAddress = "localhost" + } + if c.HTTPPort == "" { + c.HTTPPort = "9090" + } + if c.AdminPort == "" { + c.AdminPort = "9091" + } + if c.TelemetryTracingSampleRate == 0 { + c.TelemetryTracingSampleRate = 10_000 + } + return c +} + +// pointer returns the address of v and makes it easy to take the address of a +// predeclared identifier. Compare: +// +// t := true +// pt := &t +// +// vs +// +// pt := pointer(true) +func pointer[T any](v T) *T { + return &v +} + +func invokeProxyCommand(args []string) (*Command, error) { + 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(args) + + err := c.Execute() + + return c, err +} + +func TestUserAgentWithVersionEnvVar(t *testing.T) { + os.Setenv("CSQL_PROXY_USER_AGENT", "cloud-sql-proxy-operator/0.0.1") + defer os.Unsetenv("CSQL_PROXY_USER_AGENT") + + cmd, err := invokeProxyCommand([]string{"proj:region:inst"}) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + want := "cloud-sql-proxy-operator/0.0.1" + got := cmd.conf.UserAgent + if !strings.Contains(got, want) { + t.Errorf("expected user agent to contain: %v; got: %v", want, got) + } +} + +func TestUserAgent(t *testing.T) { + cmd, err := invokeProxyCommand( + []string{ + "--user-agent", + "cloud-sql-proxy-operator/0.0.1", + "proj:region:inst", + }, + ) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + want := "cloud-sql-proxy-operator/0.0.1" + got := cmd.conf.UserAgent + if !strings.Contains(got, want) { + t.Errorf("expected userAgent to contain: %v; got: %v", want, got) + } +} + +func TestNewCommandArguments(t *testing.T) { + 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", + }), + }, + { + desc: "using the JSON credentials", + args: []string{"--json-credentials", `{"json":"goes-here"}`, "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsJSON: `{"json":"goes-here"}`, + }), + }, + { + desc: "using the (short) JSON credentials", + args: []string{"-j", `{"json":"goes-here"}`, "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsJSON: `{"json":"goes-here"}`, + }), + }, + { + desc: "using the gcloud auth flag", + args: []string{"--gcloud-auth", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + GcloudAuth: true, + }), + }, + { + desc: "using the (short) gcloud auth flag", + args: []string{"-g", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + GcloudAuth: true, + }), + }, + { + desc: "using the api-endpoint flag without trailing slash", + args: []string{"--sqladmin-api-endpoint", "https://test.googleapis.com", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + APIEndpointURL: "https://test.googleapis.com/", + }), + }, + { + desc: "using the api-endpoint flag with trailing slash", + args: []string{"--sqladmin-api-endpoint", "https://test.googleapis.com/", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + APIEndpointURL: "https://test.googleapis.com/", + }), + }, + { + desc: "using the universe domain flag", + args: []string{"--universe-domain", "test-universe.test", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + UniverseDomain: "test-universe.test", + }), + }, + { + desc: "using the unix socket flag", + args: []string{"--unix-socket", "/path/to/dir/", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + UnixSocket: "/path/to/dir/", + }), + }, + { + desc: "using the (short) unix socket flag", + args: []string{"-u", "/path/to/dir/", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + UnixSocket: "/path/to/dir/", + }), + }, + { + desc: "using the unix socket query param", + args: []string{"proj:region:inst?unix-socket=/path/to/dir/"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + UnixSocket: "/path/to/dir/", + }}, + }), + }, + { + desc: "using the unix socket path query param", + args: []string{"proj:region:inst?unix-socket-path=/path/to/file"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + UnixSocketPath: "/path/to/file", + }}, + }), + }, + { + desc: "using the iam authn login flag", + args: []string{"--auto-iam-authn", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + IAMAuthN: true, + }), + }, + { + desc: "using the (short) iam authn login flag", + args: []string{"-i", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + IAMAuthN: true, + }), + }, + { + desc: "using the iam authn login query param", + args: []string{"proj:region:inst?auto-iam-authn=true"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + IAMAuthN: pointer(true), + }}, + }), + }, + { + desc: "enabling structured logging", + args: []string{"--structured-logs", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + StructuredLogs: true, + }), + }, + { + desc: "using the max connections flag", + args: []string{"--max-connections", "1", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + MaxConnections: 1, + }), + }, + { + desc: "using min-sigterm-delay flag", + args: []string{"--min-sigterm-delay", "10s", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + WaitBeforeClose: 10 * time.Second, + }), + }, + { + desc: "using wait after signterm flag", + args: []string{"--max-sigterm-delay", "10s", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + WaitOnClose: 10 * time.Second, + }), + }, + { + desc: "using the private-ip flag", + args: []string{"--private-ip", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + PrivateIP: true, + }), + }, + { + desc: "using the private-ip flag query param", + args: []string{"proj:region:inst?private-ip=true"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + PrivateIP: pointer(true), + }}, + }), + }, + { + desc: "using the private-ip flag with query param override", + args: []string{"--private-ip", "proj:region:inst?private-ip=false"}, + want: withDefaults(&proxy.Config{ + PrivateIP: true, + Instances: []proxy.InstanceConnConfig{{ + PrivateIP: pointer(false), + }}, + }), + }, + { + desc: "using the psc flag", + args: []string{"--psc", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + PSC: true, + }), + }, + { + desc: "using the psc flag query param", + args: []string{"proj:region:inst?psc=true"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + PSC: pointer(true), + }}, + }), + }, + { + desc: "using the quota project flag", + args: []string{"--quota-project", "proj", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + QuotaProject: "proj", + }), + }, + { + desc: "using the impersonate service account flag", + args: []string{"--impersonate-service-account", + "sv1@developer.gserviceaccount.com", + "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + ImpersonationChain: "sv1@developer.gserviceaccount.com", + }), + }, + { + desc: "using the debug flag", + args: []string{"--debug", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Debug: true, + }), + }, + { + desc: "using the lazy refresh flag", + args: []string{"--lazy-refresh", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + LazyRefresh: true, + }), + }, + { + desc: "using the admin port flag", + args: []string{"--admin-port", "7777", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + AdminPort: "7777", + }), + }, + { + desc: "using the quitquitquit flag", + args: []string{"--quitquitquit", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + QuitQuitQuit: true, + }), + }, + { + desc: "using the login-token flag", + args: []string{ + "--auto-iam-authn", + "--token", "MYTOK", + "--login-token", "MYLOGINTOKEN", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + IAMAuthN: true, + Token: "MYTOK", + LoginToken: "MYLOGINTOKEN", + }), + }, + { + desc: "using the auto-ip flag", + args: []string{"--auto-ip", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + AutoIP: true, + }), + }, + { + desc: "using the run-connection-test flag", + args: []string{"--run-connection-test", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + RunConnectionTest: true, + }), + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + if got := c.conf; !cmp.Equal(tc.want, got) { + t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got)) + } + }) + } +} + +func TestNewCommandWithEnvironmentConfigInstanceConnectionName(t *testing.T) { + tcs := []struct { + desc string + env map[string]string + args []string + want *proxy.Config + }{ + { + desc: "with one instance connection name", + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME": "proj:reg:inst", + }, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst"}, + }}), + }, + { + desc: "with multiple instance connection names", + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_0": "proj:reg:inst0", + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_1": "proj:reg:inst1", + }, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst0"}, + {Name: "proj:reg:inst1"}, + }}), + }, + { + desc: "with query params", + + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_0": "proj:reg:inst0?auto-iam-authn=true", + }, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst0", IAMAuthN: pointer(true)}, + }}), + }, + { + desc: "when the index skips a number", + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_0": "proj:reg:inst0", + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_2": "proj:reg:inst1", + }, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst0"}, + }}), + }, + { + desc: "when there are CLI args provided", + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME": "proj:reg:inst0", + }, + args: []string{"myotherproj:myreg:myinst"}, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "myotherproj:myreg:myinst"}, + }}), + }, + { + desc: "when only an index instance connection name is defined", + env: map[string]string{ + "CSQL_PROXY_INSTANCE_CONNECTION_NAME_0": "proj:reg:inst0", + }, + want: withDefaults(&proxy.Config{Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst0"}, + }}), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + var cleanup []string + for k, v := range tc.env { + os.Setenv(k, v) + cleanup = append(cleanup, k) + } + defer func() { + for _, k := range cleanup { + os.Unsetenv(k) + } + }() + + c, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + if got := c.conf; !cmp.Equal(tc.want, got) { + t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got)) + } + }) + } +} + +func TestNewCommandWithEnvironmentConfig(t *testing.T) { + tcs := []struct { + desc string + envName string + envValue string + want *proxy.Config + }{ + { + desc: "using the address envvar", + envName: "CSQL_PROXY_ADDRESS", + envValue: "0.0.0.0", + want: withDefaults(&proxy.Config{ + Addr: "0.0.0.0", + }), + }, + { + desc: "using the port envvar", + envName: "CSQL_PROXY_PORT", + envValue: "6000", + want: withDefaults(&proxy.Config{ + Port: 6000, + }), + }, + { + desc: "using the token envvar", + envName: "CSQL_PROXY_TOKEN", + envValue: "MYCOOLTOKEN", + want: withDefaults(&proxy.Config{ + Token: "MYCOOLTOKEN", + }), + }, + { + desc: "using the credentiale file envvar", + envName: "CSQL_PROXY_CREDENTIALS_FILE", + envValue: "/path/to/file", + want: withDefaults(&proxy.Config{ + CredentialsFile: "/path/to/file", + }), + }, + { + desc: "using the JSON credentials", + envName: "CSQL_PROXY_JSON_CREDENTIALS", + envValue: `{"json":"goes-here"}`, + want: withDefaults(&proxy.Config{ + CredentialsJSON: `{"json":"goes-here"}`, + }), + }, + { + desc: "using the gcloud auth envvar", + envName: "CSQL_PROXY_GCLOUD_AUTH", + envValue: "true", + want: withDefaults(&proxy.Config{ + GcloudAuth: true, + }), + }, + { + desc: "using the api-endpoint envvar", + envName: "CSQL_PROXY_SQLADMIN_API_ENDPOINT", + envValue: "https://test.googleapis.com/", + want: withDefaults(&proxy.Config{ + APIEndpointURL: "https://test.googleapis.com/", + }), + }, + { + desc: "using the unix socket envvar", + envName: "CSQL_PROXY_UNIX_SOCKET", + envValue: "/path/to/dir/", + want: withDefaults(&proxy.Config{ + UnixSocket: "/path/to/dir/", + }), + }, + { + desc: "using the iam authn login envvar", + envName: "CSQL_PROXY_AUTO_IAM_AUTHN", + envValue: "true", + want: withDefaults(&proxy.Config{ + IAMAuthN: true, + }), + }, + { + desc: "enabling structured logging", + envName: "CSQL_PROXY_STRUCTURED_LOGS", + envValue: "true", + want: withDefaults(&proxy.Config{ + StructuredLogs: true, + }), + }, + { + desc: "using the max connections envvar", + envName: "CSQL_PROXY_MAX_CONNECTIONS", + envValue: "1", + want: withDefaults(&proxy.Config{ + MaxConnections: 1, + }), + }, + { + desc: "using wait after signterm envvar", + envName: "CSQL_PROXY_MAX_SIGTERM_DELAY", + envValue: "10s", + want: withDefaults(&proxy.Config{ + WaitOnClose: 10 * time.Second, + }), + }, + { + desc: "using the private-ip envvar", + envName: "CSQL_PROXY_PRIVATE_IP", + envValue: "true", + want: withDefaults(&proxy.Config{ + PrivateIP: true, + }), + }, + { + desc: "using the psc envvar", + envName: "CSQL_PROXY_PSC", + envValue: "true", + want: withDefaults(&proxy.Config{ + PSC: true, + }), + }, + { + desc: "using the quota project envvar", + envName: "CSQL_PROXY_QUOTA_PROJECT", + envValue: "proj", + want: withDefaults(&proxy.Config{ + QuotaProject: "proj", + }), + }, + { + desc: "using the impersonate service account envvar", + envName: "CSQL_PROXY_IMPERSONATE_SERVICE_ACCOUNT", + envValue: "sv1@developer.gserviceaccount.com", + want: withDefaults(&proxy.Config{ + ImpersonationChain: "sv1@developer.gserviceaccount.com", + }), + }, + { + desc: "using the disable traces envvar", + envName: "CSQL_PROXY_DISABLE_TRACES", + envValue: "true", + want: withDefaults(&proxy.Config{ + DisableTraces: true, + }), + }, + { + desc: "using the telemetry sample rate envvar", + envName: "CSQL_PROXY_TELEMETRY_SAMPLE_RATE", + envValue: "500", + want: withDefaults(&proxy.Config{ + TelemetryTracingSampleRate: 500, + }), + }, + { + desc: "using the disable metrics envvar", + envName: "CSQL_PROXY_DISABLE_METRICS", + envValue: "true", + want: withDefaults(&proxy.Config{ + DisableMetrics: true, + }), + }, + { + desc: "using the telemetry project envvar", + envName: "CSQL_PROXY_TELEMETRY_PROJECT", + envValue: "mycoolproject", + want: withDefaults(&proxy.Config{ + TelemetryProject: "mycoolproject", + }), + }, + { + desc: "using the telemetry prefix envvar", + envName: "CSQL_PROXY_TELEMETRY_PREFIX", + envValue: "myprefix", + want: withDefaults(&proxy.Config{ + TelemetryPrefix: "myprefix", + }), + }, + { + desc: "using the prometheus envvar", + envName: "CSQL_PROXY_PROMETHEUS", + envValue: "true", + want: withDefaults(&proxy.Config{ + Prometheus: true, + }), + }, + { + desc: "using the prometheus namespace envvar", + envName: "CSQL_PROXY_PROMETHEUS_NAMESPACE", + envValue: "myns", + want: withDefaults(&proxy.Config{ + PrometheusNamespace: "myns", + }), + }, + { + desc: "using the health check envvar", + envName: "CSQL_PROXY_HEALTH_CHECK", + envValue: "true", + want: withDefaults(&proxy.Config{ + HealthCheck: true, + }), + }, + { + desc: "using the http address envvar", + envName: "CSQL_PROXY_HTTP_ADDRESS", + envValue: "0.0.0.0", + want: withDefaults(&proxy.Config{ + HTTPAddress: "0.0.0.0", + }), + }, + { + desc: "using the http port envvar", + envName: "CSQL_PROXY_HTTP_PORT", + envValue: "5555", + want: withDefaults(&proxy.Config{ + HTTPPort: "5555", + }), + }, + { + desc: "using the debug envvar", + envName: "CSQL_PROXY_DEBUG", + envValue: "true", + want: withDefaults(&proxy.Config{ + Debug: true, + }), + }, + { + desc: "using the admin port envvar", + envName: "CSQL_PROXY_ADMIN_PORT", + envValue: "7777", + want: withDefaults(&proxy.Config{ + AdminPort: "7777", + }), + }, + { + desc: "using the quitquitquit envvar", + envName: "CSQL_PROXY_QUITQUITQUIT", + envValue: "true", + want: withDefaults(&proxy.Config{ + QuitQuitQuit: true, + }), + }, + { + desc: "using the auto-ip envvar", + envName: "CSQL_PROXY_AUTO_IP", + envValue: "true", + want: withDefaults(&proxy.Config{ + AutoIP: true, + }), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + os.Setenv(tc.envName, tc.envValue) + defer os.Unsetenv(tc.envName) + + c, err := invokeProxyCommand([]string{"proj:region:inst"}) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + if got := c.conf; !cmp.Equal(tc.want, got) { + t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got)) + } + }) + } +} + +func TestAutoIAMAuthNQueryParams(t *testing.T) { + tcs := []struct { + desc string + args []string + want *bool + }{ + { + desc: "when the query string is absent", + args: []string{"proj:region:inst"}, + want: nil, + }, + { + desc: "when the query string is true", + args: []string{"proj:region:inst?auto-iam-authn=true"}, + want: pointer(true), + }, + { + desc: "when the query string is (short) t", + args: []string{"proj:region:inst?auto-iam-authn=t"}, + want: pointer(true), + }, + { + desc: "when the query string is false", + args: []string{"proj:region:inst?auto-iam-authn=false"}, + want: pointer(false), + }, + { + desc: "when the query string is (short) f", + args: []string{"proj:region:inst?auto-iam-authn=f"}, + want: pointer(false), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("command.Execute: %v", err) + } + if tc.want == nil && c.conf.Instances[0].IAMAuthN == nil { + return + } + if got := c.conf.Instances[0].IAMAuthN; *got != *tc.want { + t.Errorf("args = %v, want = %v, got = %v", tc.args, *tc.want, *got) + } + }) + } +} + +func TestPrivateIPQueryParams(t *testing.T) { + tcs := []struct { + desc string + args []string + want *bool + }{ + { + desc: "when the query string is absent", + args: []string{"proj:region:inst"}, + want: nil, + }, + { + desc: "when the query string has no value", + args: []string{"proj:region:inst?private-ip"}, + want: pointer(true), + }, + { + desc: "when the query string is true", + args: []string{"proj:region:inst?private-ip=true"}, + want: pointer(true), + }, + { + desc: "when the query string is True", + args: []string{"proj:region:inst?private-ip=True"}, + want: pointer(true), + }, + { + desc: "when the query string is (short) T", + args: []string{"proj:region:inst?private-ip=T"}, + want: pointer(true), + }, + { + desc: "when the query string is (short) t", + args: []string{"proj:region:inst?private-ip=t"}, + want: pointer(true), + }, + { + desc: "when the query string is false", + args: []string{"proj:region:inst?private-ip=false"}, + want: pointer(false), + }, + { + desc: "when the query string is (short) f", + args: []string{"proj:region:inst?private-ip=f"}, + want: pointer(false), + }, + { + desc: "when the query string is False", + args: []string{"proj:region:inst?private-ip=False"}, + want: pointer(false), + }, + { + desc: "when the query string is (short) F", + args: []string{"proj:region:inst?private-ip=F"}, + want: pointer(false), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("command.Execute: %v", err) + } + if tc.want == nil && c.conf.Instances[0].PrivateIP == nil { + return + } + if got := c.conf.Instances[0].PrivateIP; *got != *tc.want { + t.Errorf("args = %v, want = %v, got = %v", tc.args, *tc.want, *got) + } + }) + } +} + +func TestPSCQueryParams(t *testing.T) { + tcs := []struct { + desc string + args []string + want *bool + }{ + { + desc: "when the query string is absent", + args: []string{"proj:region:inst"}, + want: nil, + }, + { + desc: "when the query string has no value", + args: []string{"proj:region:inst?psc"}, + want: pointer(true), + }, + { + desc: "when the query string is true", + args: []string{"proj:region:inst?psc=true"}, + want: pointer(true), + }, + { + desc: "when the query string is True", + args: []string{"proj:region:inst?psc=True"}, + want: pointer(true), + }, + { + desc: "when the query string is (short) T", + args: []string{"proj:region:inst?psc=T"}, + want: pointer(true), + }, + { + desc: "when the query string is (short) t", + args: []string{"proj:region:inst?psc=t"}, + want: pointer(true), + }, + { + desc: "when the query string is false", + args: []string{"proj:region:inst?psc=false"}, + want: pointer(false), + }, + { + desc: "when the query string is (short) f", + args: []string{"proj:region:inst?psc=f"}, + want: pointer(false), + }, + { + desc: "when the query string is False", + args: []string{"proj:region:inst?psc=False"}, + want: pointer(false), + }, + { + desc: "when the query string is (short) F", + args: []string{"proj:region:inst?psc=F"}, + want: pointer(false), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := invokeProxyCommand(tc.args) + if err != nil { + t.Fatalf("command.Execute: %v", err) + } + if tc.want == nil && c.conf.Instances[0].PSC == nil { + return + } + if got := c.conf.Instances[0].PSC; *got != *tc.want { + t.Errorf("args = %v, want = %v, got = %v", tc.args, *tc.want, *got) + } + }) + } +} + +func TestNewCommandWithErrors(t *testing.T) { + tcs := []struct { + desc string + 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 are set", + args: []string{ + "--token", "my-token", + "--credentials-file", "/path/to/file", "proj:region:inst"}, + }, + { + desc: "when both token and gcloud auth are set", + args: []string{ + "--token", "my-token", + "--gcloud-auth", "proj:region:inst"}, + }, + { + desc: "when both gcloud auth and auto-iam-authn are set", + args: []string{ + "--auto-iam-authn", + "--gcloud-auth", "proj:region:inst"}, + }, + { + desc: "when both gcloud auth and credentials file are set", + args: []string{ + "--gcloud-auth", + "--credentials-file", "/path/to/file", "proj:region:inst"}, + }, + { + desc: "when both token and credentials JSON are set", + args: []string{ + "--token", "a-token", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, + }, + { + desc: "when both credentials file and credentials JSON are set", + args: []string{ + "--credentials-file", "/a/file", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, + }, + { + desc: "when both gcloud auth and credentials JSON are set", + args: []string{ + "--gcloud-auth", + "--json-credentials", `{"json":"here"}`, "proj:region:inst"}, + }, + { + desc: "when the unix socket query param contains multiple values", + args: []string{"proj:region:inst?unix-socket=/one&unix-socket=/two"}, + }, + { + desc: "using the unix socket flag with addr", + args: []string{"-u", "/path/to/dir/", "-a", "127.0.0.1", "proj:region:inst"}, + }, + { + desc: "using the unix socket flag with port", + args: []string{"-u", "/path/to/dir/", "-p", "5432", "proj:region:inst"}, + }, + { + desc: "using the unix socket and unix-socket-path", + args: []string{"proj:region:inst?unix-socket=/path&unix-socket-path=/another/path"}, + }, + { + desc: "using the unix socket and addr query params", + args: []string{"proj:region:inst?unix-socket=/path&address=127.0.0.1"}, + }, + { + desc: "using the unix socket path and addr query params", + args: []string{"proj:region:inst?unix-socket-path=/path&address=127.0.0.1"}, + }, + { + desc: "using the unix socket and port query params", + args: []string{"proj:region:inst?unix-socket=/path&port=5000"}, + }, + { + desc: "using the unix socket path and port query params", + args: []string{"proj:region:inst?unix-socket-path=/path&port=5000"}, + }, + { + desc: "when the iam authn login query param contains multiple values", + args: []string{"proj:region:inst?auto-iam-authn=true&auto-iam-authn=false"}, + }, + { + desc: "when the iam authn login query param is bogus", + args: []string{"proj:region:inst?auto-iam-authn=nope"}, + }, + { + desc: "using an invalid url for sqladmin-api-endpoint", + args: []string{"--sqladmin-api-endpoint", "https://user:abc{DEf1=ghi@example.com:5432/db?sslmode=require", "proj:region:inst"}, + }, + { + desc: "using fuse-tmp-dir without fuse", + args: []string{"--fuse-tmp-dir", "/mydir"}, + }, + { + desc: "using --auto-iam-authn with just token flag", + args: []string{"--auto-iam-authn", "--token", "MYTOKEN", "p:r:i"}, + }, + { + desc: "using the --login-token without --token and --auto-iam-authn", + args: []string{"--login-token", "MYTOKEN", "p:r:i"}, + }, + { + desc: "using --token and --login-token without --auto-iam-authn", + args: []string{ + "--token", "MYTOKEN", + "--login-token", "MYLOGINTOKEN", "p:r:i"}, + }, + { + desc: "using --private-ip with --auto-ip", + args: []string{ + "--private-ip", "--auto-ip", + "p:r:i", + }, + }, + { + desc: "using private-ip query param with --auto-ip", + args: []string{ + "--auto-ip", + "p:r:i?private-ip=true", + }, + }, + { + desc: "using private IP and psc query params", + args: []string{"p:r:i?private-ip=true&psc=true"}, + }, + { + desc: "using --private-ip with --psc", + args: []string{ + "--private-ip", "--psc", + "p:r:i", + }, + }, + { + desc: "run-connection-test with fuse", + args: []string{ + "--run-connection-test", + "--fuse", "myfusedir", + }, + }, + { + desc: "using both --sqladmin-api-endpoint and --universe-domain", + args: []string{ + "--sqladmin-api-endpoint", "https://sqladmin.googleapis.com", + "--universe-domain", "test-universe.test", "proj:region:inst"}, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, err := invokeProxyCommand(tc.args) + 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, _ string, _ ...cloudsqlconn.DialOption) (net.Conn, error) { + return nil, errors.New("spy dialer does not dial") +} + +func (s *spyDialer) EngineVersion(_ 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.Is(err, errSigInt) { + t.Fatalf("want errSigInt, got = %v", err) + } + + if got := s.instance(); got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} + +func tryDial(method, addr string) (*http.Response, error) { + var ( + resp *http.Response + attempts int + err error + ) + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + req := &http.Request{Method: method, URL: u} + // Never wait longer than 30 seconds for an HTTP response. + cl := &http.Client{Timeout: 30 * time.Second} + for { + if attempts > 10 { + return resp, err + } + resp, err = cl.Do(req) + if err != nil { + attempts++ + time.Sleep(time.Second) + continue + } + return resp, err + } +} + +func TestPrometheusMetricsEndpoint(t *testing.T) { + c := NewCommand(WithDialer(&spyDialer{})) + // Keep the test output quiet + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{"--prometheus", "my-project:my-region:my-instance"}) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + go c.ExecuteContext(ctx) + + // try to dial metrics server for a max of ~10s to give the proxy time to + // start up. + resp, err := tryDial("GET", "http://localhost:9090/metrics") // default port set by http-port flag + if err != nil { + t.Fatalf("failed to dial metrics endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected a 200 status, got = %v", resp.StatusCode) + } +} + +func TestPProfServer(t *testing.T) { + c := NewCommand(WithDialer(&spyDialer{})) + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{"--debug", "--admin-port", "9191", "my-project:my-region:my-instance"}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go c.ExecuteContext(ctx) + resp, err := tryDial("GET", "http://localhost:9191/debug/pprof/") + if err != nil { + t.Fatalf("failed to dial endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected a 200 status, got = %v", resp.StatusCode) + } +} + +func TestQuitQuitQuitHTTPPost(t *testing.T) { + c := NewCommand(WithDialer(&spyDialer{})) + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{"--quitquitquit", "--admin-port", "9192", "my-project:my-region:my-instance"}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + errCh := make(chan error) + go func() { + err := c.ExecuteContext(ctx) + errCh <- err + }() + resp, err := tryDial("HEAD", "http://localhost:9192/quitquitquit") + if err != nil { + t.Fatalf("failed to dial endpoint: %v", err) + } + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("expected a 400 status, got = %v", resp.StatusCode) + } + resp, err = tryDial("POST", "http://localhost:9192/quitquitquit") + if err != nil { + t.Fatalf("failed to dial endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected a 200 status, got = %v", resp.StatusCode) + } + + var gotErr error + select { + case err := <-errCh: + gotErr = err + case <-time.After(30 * time.Second): + t.Fatal("timeout waiting for error") + } + + if !errors.Is(gotErr, errQuitQuitQuit) { + t.Fatalf("want = %v, got = %v", errQuitQuitQuit, gotErr) + } +} + +func TestQuitQuitQuitHTTPGet(t *testing.T) { + c := NewCommand(WithDialer(&spyDialer{})) + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{"--quitquitquit", "--admin-port", "9194", "my-project:my-region:my-instance"}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + errCh := make(chan error) + go func() { + err := c.ExecuteContext(ctx) + errCh <- err + }() + + resp, err := tryDial("GET", "http://localhost:9194/quitquitquit") + if err != nil { + t.Fatalf("failed to dial endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected a 200 status, got = %v", resp.StatusCode) + } + + var gotErr error + select { + case err := <-errCh: + gotErr = err + case <-time.After(30 * time.Second): + t.Fatal("timeout waiting for error") + } + + if !errors.Is(gotErr, errQuitQuitQuit) { + t.Fatalf("want = %v, got = %v", errQuitQuitQuit, gotErr) + } +} + +type errorDialer struct { + spyDialer +} + +var errCloseFailed = errors.New("close failed") + +func (*errorDialer) Close() error { + return errCloseFailed +} + +func TestQuitQuitQuitWithErrors(t *testing.T) { + c := NewCommand(WithDialer(&errorDialer{})) + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{ + "--quitquitquit", "--admin-port", "9193", + "my-project:my-region:my-instance", + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + errCh := make(chan error) + go func() { + err := c.ExecuteContext(ctx) + errCh <- err + }() + resp, err := tryDial("POST", "http://localhost:9193/quitquitquit") + if err != nil { + t.Fatalf("failed to dial endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected a 200 status, got = %v", resp.StatusCode) + } + // The returned error is the error from closing the dialer. + got := <-errCh + if !strings.Contains(got.Error(), "close failed") { + t.Fatalf("want = %v, got = %v", errCloseFailed, got) + } +} diff --git a/proxy/fuse/fuse_windows.go b/cmd/root_windows_test.go similarity index 56% rename from proxy/fuse/fuse_windows.go rename to cmd/root_windows_test.go index 93e0c5918..78b17674d 100644 --- a/proxy/fuse/fuse_windows.go +++ b/cmd/root_windows_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 Google Inc. All Rights Reserved. +// 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. @@ -12,20 +12,25 @@ // 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 +package cmd import ( - "errors" - "io" + "testing" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" + "github.com/spf13/cobra" ) -func Supported() bool { - return false -} +func TestWindowsDoesNotSupportFUSE(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([]string{"--fuse"}) -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") + err := c.Execute() + if err == nil { + t.Fatal("want error != nil, got = nil") + } } diff --git a/cmd/shutdown_test.go b/cmd/shutdown_test.go new file mode 100644 index 000000000..03bd44cad --- /dev/null +++ b/cmd/shutdown_test.go @@ -0,0 +1,67 @@ +// Copyright 2025 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 ( + "net" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestShutdownCommand(t *testing.T) { + shutdownCh := make(chan bool, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("want = POST, got = %v", r.Method) + } + w.WriteHeader(http.StatusOK) + shutdownCh <- true + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + _, port, err := net.SplitHostPort(server.Listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + + _, err = invokeProxyCommand([]string{ + "shutdown", + "--admin-port", port, + }) + if err != nil { + t.Fatalf("invokeProxyCommand failed: %v", err) + } + + select { + case <-shutdownCh: + // success + case <-time.After(1 * time.Second): + t.Fatal("server did not receive shutdown request") + } +} + +func TestShutdownCommandFails(t *testing.T) { + _, err := invokeProxyCommand([]string{ + "shutdown", + // assuming default host and port + "--wait=100ms", + }) + if err == nil { + t.Fatal("shutdown should fail when endpoint does not respond") + } +} diff --git a/cmd/testdata/config-json.json b/cmd/testdata/config-json.json new file mode 100644 index 000000000..6a8ea08dc --- /dev/null +++ b/cmd/testdata/config-json.json @@ -0,0 +1,4 @@ +{ + "instance-connection-name": "proj:region:inst", + "debug": true +} diff --git a/cmd/testdata/config-toml.toml b/cmd/testdata/config-toml.toml new file mode 100644 index 000000000..2cad8bc6b --- /dev/null +++ b/cmd/testdata/config-toml.toml @@ -0,0 +1,5 @@ +instance-connection-name = "proj:region:inst" +debug = true +port = "5555" +debug-logs = true +auto-iam-authn = true diff --git a/cmd/testdata/config-yaml.yaml b/cmd/testdata/config-yaml.yaml new file mode 100644 index 000000000..4ab85f439 --- /dev/null +++ b/cmd/testdata/config-yaml.yaml @@ -0,0 +1,16 @@ +# Copyright 2024 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. + +instance-connection-name: "proj:region:inst" +debug: true diff --git a/cmd/testdata/two-instances.toml b/cmd/testdata/two-instances.toml new file mode 100644 index 000000000..ddd670f5e --- /dev/null +++ b/cmd/testdata/two-instances.toml @@ -0,0 +1,2 @@ +instance-connection-name-0 = "x:y:z" +instance-connection-name-1 = "a:b:c" diff --git a/cmd/version.txt b/cmd/version.txt new file mode 100644 index 000000000..3fe7dd19b --- /dev/null +++ b/cmd/version.txt @@ -0,0 +1 @@ +2.21.1 diff --git a/cmd/wait_test.go b/cmd/wait_test.go new file mode 100644 index 000000000..fcc332b67 --- /dev/null +++ b/cmd/wait_test.go @@ -0,0 +1,68 @@ +// Copyright 2023 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 ( + "io" + "net" + "testing" + "time" +) + +func TestWaitCommandFlags(t *testing.T) { + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + host, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + go func() { + conn, err := ln.Accept() + if err != nil { + return + } + defer conn.Close() + // Use a read deadline to produce read error + conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + // Read client request first. + io.ReadAll(conn) + // Write a generic 200 response back. + conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) + }() + + _, err = invokeProxyCommand([]string{ + "wait", + "--http-address", host, + "--http-port", port, + "--max=1s", + }) + if err != nil { + t.Fatal(err) + } +} + +func TestWaitCommandFails(t *testing.T) { + _, err := invokeProxyCommand([]string{ + "wait", + // assuming default host and port + "--max=100ms", + }) + if err == nil { + t.Fatal("wait should fail when endpoint does not respond") + } +} diff --git a/docs/cmd/cloud-sql-proxy.md b/docs/cmd/cloud-sql-proxy.md new file mode 100644 index 000000000..bb4c42484 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy.md @@ -0,0 +1,299 @@ +## cloud-sql-proxy + +cloud-sql-proxy authorizes and encrypts connections to Cloud SQL. + +### Synopsis + + +Overview + + The Cloud SQL Auth Proxy is a utility for ensuring secure connections to + your Cloud SQL instances. It provides IAM authorization, allowing you to + control who can connect to your instance through IAM permissions, and TLS + 1.3 encryption, without having to manage certificates. + + NOTE: The Proxy does not configure the network. You MUST ensure the Proxy + can reach your Cloud SQL instance, either by deploying it in a VPC that has + access to your Private IP instance, or by configuring Public IP. + + For every provided instance connection name, the Proxy creates: + + - a socket that mimics a database running locally, and + - an encrypted connection using TLS 1.3 back to your Cloud SQL instance. + + The Proxy uses an ephemeral certificate to establish a secure connection to + your Cloud SQL instance. The Proxy will refresh those certificates on an + hourly basis. Existing client connections are unaffected by the refresh + cycle. + +Starting the Proxy + + To start the Proxy, you will need your instance connection name, which may + be found in the Cloud SQL instance overview page or by using gcloud with the + following command: + + gcloud sql instances describe INSTANCE --format='value(connectionName)' + + For example, if your instance connection name is + "my-project:us-central1:my-db-server", starting the Proxy will be: + + ./cloud-sql-proxy my-project:us-central1:my-db-server + + By default, the Proxy will determine the database engine and start a + listener on localhost using the default database engine's port, i.e., MySQL + is 3306, Postgres is 5432, SQL Server is 1433. If multiple instances are + specified which all use the same database engine, the first will be started + on the default database port and subsequent instances will be incremented + from there (e.g., 3306, 3307, 3308, etc). To disable this behavior (and + reduce startup time), use the --port flag. All subsequent listeners will + increment from the provided value. + + All socket listeners use the localhost network interface. To override this + behavior, use the --address flag. + +Instance Level Configuration + + The Proxy supports overriding configuration on an instance-level with an + optional query string syntax using the corresponding full flag name. The + query string takes the form of a URL query string and should be appended to + the INSTANCE_CONNECTION_NAME, e.g., + + 'my-project:us-central1:my-db-server?key1=value1&key2=value2' + + When using the optional query string syntax, quotes must wrap the instance + connection name and query string to prevent conflicts with the shell. For + example, to override the address and port for one instance but otherwise use + the default behavior, use: + + ./cloud-sql-proxy \ + my-project:us-central1:my-db-server \ + 'my-project:us-central1:my-other-server?address=0.0.0.0&port=7000' + + When necessary, you may specify the full path to a Unix socket. Set the + unix-socket-path query parameter to the absolute path of the Unix socket for + the database instance. The parent directory of the unix-socket-path must + exist when the Proxy starts or else socket creation will fail. For Postgres + instances, the Proxy will ensure that the last path element is + '.s.PGSQL.5432' appending it if necessary. For example, + + ./cloud-sql-proxy \ + 'my-project:us-central1:my-db-server?unix-socket-path=/path/to/socket' + +Health checks + + When enabling the --health-check flag, the Proxy will start an HTTP server + on localhost with three endpoints: + + - /startup: Returns 200 status when the Proxy has finished starting up. + Otherwise returns 503 status. + + - /readiness: Returns 200 status when the Proxy has started, has available + connections if max connections have been set with the --max-connections + flag, and when the Proxy can connect to all registered instances. Otherwise, + returns a 503 status. + + - /liveness: Always returns 200 status. If this endpoint is not responding, + the Proxy is in a bad state and should be restarted. + + To configure the address, use --http-address. To configure the port, use + --http-port. + +Service Account Impersonation + + The Proxy supports service account impersonation with the + --impersonate-service-account flag and matches gclouds flag. When enabled, + all API requests are made impersonating the supplied service account. The + IAM principal must have the iam.serviceAccounts.getAccessToken permission or + the role roles/iam.serviceAccounts.serviceAccountTokenCreator. + + For example: + + ./cloud-sql-proxy \ + --impersonate-service-account=impersonated@my-project.iam.gserviceaccount.com + my-project:us-central1:my-db-server + + In addition, the flag supports an impersonation delegation chain where the + value is a comma-separated list of service accounts. The first service + account in the list is the impersonation target. Each subsequent service + account is a delegate to the previous service account. When delegation is + used, each delegate must have the permissions named above on the service + account it is delegating to. + + For example: + + ./cloud-sql-proxy \ + --impersonate-service-account=SERVICE_ACCOUNT_1,SERVICE_ACCOUNT_2,SERVICE_ACCOUNT_3 + my-project:us-central1:my-db-server + + In this example, the environment's IAM principal impersonates + SERVICE_ACCOUNT_3 which impersonates SERVICE_ACCOUNT_2 which then + impersonates the target SERVICE_ACCOUNT_1. + +Configuration using environment variables + + Instead of using CLI flags, the Proxy may be configured using environment + variables. Each environment variable uses "CSQL_PROXY" as a prefix and is + the uppercase version of the flag using underscores as word delimiters. For + example, the --auto-iam-authn flag may be set with the environment variable + CSQL_PROXY_AUTO_IAM_AUTHN. An invocation of the Proxy using environment + variables would look like the following: + + CSQL_PROXY_AUTO_IAM_AUTHN=true \ + ./cloud-sql-proxy my-project:us-central1:my-db-server + + In addition to CLI flags, instance connection names may also be specified + with environment variables. If invoking the Proxy with only one instance + connection name, use CSQL_PROXY_INSTANCE_CONNECTION_NAME. For example: + + CSQL_PROXY_INSTANCE_CONNECTION_NAME=my-project:us-central1:my-db-server \ + ./cloud-sql-proxy + + If multiple instance connection names are used, add the index of the + instance connection name as a suffix. For example: + + CSQL_PROXY_INSTANCE_CONNECTION_NAME_0=my-project:us-central1:my-db-server \ + CSQL_PROXY_INSTANCE_CONNECTION_NAME_1=my-other-project:us-central1:my-other-server \ + ./cloud-sql-proxy + +Configuration using a configuration file + + Instead of using CLI flags, the Proxy may be configured using a configuration + file. The configuration file is a TOML, YAML or JSON file with the same keys + as the environment variables. The configuration file is specified with the + --config-file flag. An invocation of the Proxy using a configuration file + would look like the following: + + ./cloud-sql-proxy --config-file=config.toml + + The configuration file may look like the following: + + instance-connection-name = "my-project:us-central1:my-server-instance" + auto-iam-authn = true + + If multiple instance connection names are used, add the index of the + instance connection name as a suffix. For example: + + instance-connection-name-0 = "my-project:us-central1:my-db-server" + instance-connection-name-1 = "my-other-project:us-central1:my-other-server" + + The configuration file may also contain the same keys as the environment + variables and flags. For example: + + auto-iam-authn = true + debug = true + max-connections = 5 + +Localhost Admin Server + + The Proxy includes support for an admin server on localhost. By default, + the admin server is not enabled. To enable the server, pass the --debug or + --quitquitquit flag. This will start the server on localhost at port 9091. + To change the port, use the --admin-port flag. + + When --debug is set, the admin server enables Go's profiler available at + /debug/pprof/. + + See the documentation on pprof for details on how to use the + profiler at https://pkg.go.dev/net/http/pprof. + + When --quitquitquit is set, the admin server adds an endpoint at + /quitquitquit. The admin server exits gracefully when it receives a GET or POST + request at /quitquitquit. + +Debug logging + + On occasion, it can help to enable debug logging which will report on + internal certificate refresh operations. To enable debug logging, use: + + ./cloud-sql-proxy --debug-logs + +Waiting for Startup + + See the wait subcommand's help for details. + +(*) indicates a flag that may be used as a query parameter + +Third Party Licenses + + To view all licenses for third party dependencies used within this + distribution please see: + + https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.21.1/third_party/licenses.tar.gz + + +``` +cloud-sql-proxy INSTANCE_CONNECTION_NAME... [flags] +``` + +### Options + +``` + -a, --address string (*) Address to bind Cloud SQL instance listeners. (default "127.0.0.1") + --admin-port string Port for localhost-only admin server (default "9091") + -i, --auto-iam-authn (*) Enables Automatic IAM Authentication for all instances + --auto-ip Supports legacy behavior of v1 and will try to connect to first IP + address returned by the SQL Admin API. In most cases, this flag should not be used. + Prefer default of public IP or use --private-ip instead. + --config-file string Path to a TOML file containing configuration options. + -c, --credentials-file string Use service account key file as a source of IAM credentials. + --debug Enable pprof on the localhost admin server + --debug-logs Enable debug logging + --disable-metrics Disable Cloud Monitoring integration (used with --telemetry-project) + --disable-traces Disable Cloud Trace integration (used with --telemetry-project) + --exit-zero-on-sigterm Exit with 0 exit code when Sigterm received (default is 143) + --fuse string Mount a directory at the path using FUSE to access Cloud SQL instances. + --fuse-tmp-dir string Temp dir for Unix sockets created with FUSE (default "/tmp/csql-tmp") + -g, --gcloud-auth Use gclouds user credentials as a source of IAM credentials. + NOTE: this flag is a legacy feature and generally should not be used. + Instead prefer Application Default Credentials + (enabled with: gcloud auth application-default login) which + the Proxy will then pick-up automatically. + --health-check Enables health check endpoints /startup, /liveness, and /readiness on localhost. + -h, --help Display help information for cloud-sql-proxy + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") + --impersonate-service-account string Comma separated list of service accounts to impersonate. Last value + is the target account. + -j, --json-credentials string Use service account key JSON as a source of IAM credentials. + --lazy-refresh Configure a lazy refresh where connection info is retrieved only if + the cached copy has expired. Use this setting in environments where the + CPU may be throttled and a background refresh cannot run reliably + (e.g., Cloud Run) + --login-token string Use bearer token as a database password (used with token and auto-iam-authn only) + --max-connections uint Limit the number of connections. Default is no limit. + --max-sigterm-delay duration Maximum number of seconds to wait for connections to close after receiving a TERM signal. + --min-sigterm-delay duration The number of seconds to accept new connections after receiving a TERM signal. + -p, --port int (*) Initial port for listeners. Subsequent listeners increment from this value. + --private-ip (*) Connect to the private ip address for all instances + --prometheus Enable Prometheus HTTP endpoint /metrics on localhost + --prometheus-namespace string Use the provided Prometheus namespace for metrics + --psc (*) Connect to the PSC endpoint for all instances + --quiet Log error messages only + --quitquitquit Enable quitquitquit endpoint on the localhost admin server + --quota-project string Specifies the project to use for Cloud SQL Admin API quota tracking. + The IAM principal must have the "serviceusage.services.use" permission + for the given project. See https://cloud.google.com/service-usage/docs/overview and + https://cloud.google.com/storage/docs/requester-pays + --run-connection-test Runs a connection test + against all specified instances. If an instance is unreachable, the Proxy exits with a failure + status code. + --skip-failed-instance-config If set, the Proxy will skip any instances that are invalid/unreachable ( + only applicable to Unix sockets) + --sqladmin-api-endpoint string API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com) + -l, --structured-logs Enable structured logging with LogEntry format + --telemetry-prefix string Prefix for Cloud Monitoring metrics. + --telemetry-project string Enable Cloud Monitoring and Cloud Trace with the provided project ID. + --telemetry-sample-rate int Set the Cloud Trace sample rate. A smaller number means more traces. (default 10000) + -t, --token string Use bearer token as a source of IAM credentials. + --universe-domain string Universe Domain for non-GDU environments. (default: googleapis.com) + -u, --unix-socket string (*) Enables Unix sockets for all listeners with the provided directory. + --user-agent string Space separated list of additional user agents, e.g. cloud-sql-proxy-operator/0.0.1 + -v, --version Print the cloud-sql-proxy version +``` + +### SEE ALSO + +* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell +* [cloud-sql-proxy shutdown](cloud-sql-proxy_shutdown.md) - Signal a running Proxy process to shut down +* [cloud-sql-proxy wait](cloud-sql-proxy_wait.md) - Wait for another Proxy process to start + diff --git a/docs/cmd/cloud-sql-proxy_completion.md b/docs/cmd/cloud-sql-proxy_completion.md new file mode 100644 index 000000000..8cbe02458 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_completion.md @@ -0,0 +1,31 @@ +## cloud-sql-proxy completion + +Generate the autocompletion script for the specified shell + +### Synopsis + +Generate the autocompletion script for cloud-sql-proxy for the specified shell. +See each sub-command's help for details on how to use the generated script. + + +### Options + +``` + -h, --help help for completion +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy](cloud-sql-proxy.md) - cloud-sql-proxy authorizes and encrypts connections to Cloud SQL. +* [cloud-sql-proxy completion bash](cloud-sql-proxy_completion_bash.md) - Generate the autocompletion script for bash +* [cloud-sql-proxy completion fish](cloud-sql-proxy_completion_fish.md) - Generate the autocompletion script for fish +* [cloud-sql-proxy completion powershell](cloud-sql-proxy_completion_powershell.md) - Generate the autocompletion script for powershell +* [cloud-sql-proxy completion zsh](cloud-sql-proxy_completion_zsh.md) - Generate the autocompletion script for zsh + diff --git a/docs/cmd/cloud-sql-proxy_completion_bash.md b/docs/cmd/cloud-sql-proxy_completion_bash.md new file mode 100644 index 000000000..18ccb35df --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_completion_bash.md @@ -0,0 +1,50 @@ +## cloud-sql-proxy completion bash + +Generate the autocompletion script for bash + +### Synopsis + +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(cloud-sql-proxy completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + cloud-sql-proxy completion bash > /etc/bash_completion.d/cloud-sql-proxy + +#### macOS: + + cloud-sql-proxy completion bash > $(brew --prefix)/etc/bash_completion.d/cloud-sql-proxy + +You will need to start a new shell for this setup to take effect. + + +``` +cloud-sql-proxy completion bash +``` + +### Options + +``` + -h, --help help for bash + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/cmd/cloud-sql-proxy_completion_fish.md b/docs/cmd/cloud-sql-proxy_completion_fish.md new file mode 100644 index 000000000..bfc1bce6d --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_completion_fish.md @@ -0,0 +1,41 @@ +## cloud-sql-proxy completion fish + +Generate the autocompletion script for fish + +### Synopsis + +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + cloud-sql-proxy completion fish | source + +To load completions for every new session, execute once: + + cloud-sql-proxy completion fish > ~/.config/fish/completions/cloud-sql-proxy.fish + +You will need to start a new shell for this setup to take effect. + + +``` +cloud-sql-proxy completion fish [flags] +``` + +### Options + +``` + -h, --help help for fish + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/cmd/cloud-sql-proxy_completion_powershell.md b/docs/cmd/cloud-sql-proxy_completion_powershell.md new file mode 100644 index 000000000..b74a09c4b --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_completion_powershell.md @@ -0,0 +1,38 @@ +## cloud-sql-proxy completion powershell + +Generate the autocompletion script for powershell + +### Synopsis + +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + cloud-sql-proxy completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + + +``` +cloud-sql-proxy completion powershell [flags] +``` + +### Options + +``` + -h, --help help for powershell + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/cmd/cloud-sql-proxy_completion_zsh.md b/docs/cmd/cloud-sql-proxy_completion_zsh.md new file mode 100644 index 000000000..486c624a5 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_completion_zsh.md @@ -0,0 +1,52 @@ +## cloud-sql-proxy completion zsh + +Generate the autocompletion script for zsh + +### Synopsis + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(cloud-sql-proxy completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + cloud-sql-proxy completion zsh > "${fpath[1]}/_cloud-sql-proxy" + +#### macOS: + + cloud-sql-proxy completion zsh > $(brew --prefix)/share/zsh/site-functions/_cloud-sql-proxy + +You will need to start a new shell for this setup to take effect. + + +``` +cloud-sql-proxy completion zsh [flags] +``` + +### Options + +``` + -h, --help help for zsh + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/cmd/cloud-sql-proxy_shutdown.md b/docs/cmd/cloud-sql-proxy_shutdown.md new file mode 100644 index 000000000..aa5befb24 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_shutdown.md @@ -0,0 +1,52 @@ +## cloud-sql-proxy shutdown + +Signal a running Proxy process to shut down + +### Synopsis + + +Shutting Down the Proxy + + The shutdown command signals a running Proxy process to gracefully shut + down. This is useful for scripting and for Kubernetes environments. + + The shutdown command requires that the Proxy be started in another process + with the admin server enabled. For example: + + ./cloud-sql-proxy --quitquitquit + + Invoke the shutdown command like this: + + # signals another Proxy process to shut down + ./cloud-sql-proxy shutdown + +Configuration + + If the running Proxy is configured with a non-default admin port, the + shutdown command must also be told to use the same custom value: + + ./cloud-sql-proxy shutdown --admin-port 9192 + + +``` +cloud-sql-proxy shutdown [flags] +``` + +### Options + +``` + --admin-port string port for the admin server (default "9091") + -h, --help help for shutdown +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy](cloud-sql-proxy.md) - cloud-sql-proxy authorizes and encrypts connections to Cloud SQL. + diff --git a/docs/cmd/cloud-sql-proxy_wait.md b/docs/cmd/cloud-sql-proxy_wait.md new file mode 100644 index 000000000..27d90f569 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_wait.md @@ -0,0 +1,67 @@ +## cloud-sql-proxy wait + +Wait for another Proxy process to start + +### Synopsis + + +Waiting for Proxy Startup + + Sometimes it is necessary to wait for the Proxy to start. + + To help ensure the Proxy is up and ready, the Proxy includes a wait + subcommand with an optional --max flag to set the maximum time to wait. + The wait command uses a separate Proxy's startup endpoint to determine + if the other Proxy process is ready. + + Invoke the wait command, like this: + + # waits for another Proxy process' startup endpoint to respond + ./cloud-sql-proxy wait + +Configuration + + By default, the Proxy will wait up to the maximum time for the startup + endpoint to respond. The wait command requires that the Proxy be started in + another process with the HTTP health check or Prometheus enabled. If an + alternate health check port or address is used, as in: + + ./cloud-sql-proxy \ + --http-address 0.0.0.0 \ + --http-port 9191 \ + --health-check + + Then the wait command must also be told to use the same custom values: + + ./cloud-sql-proxy wait \ + --http-address 0.0.0.0 \ + --http-port 9191 + + By default the wait command will wait 30 seconds. To alter this value, + use: + + ./cloud-sql-proxy wait --max 10s + + +``` +cloud-sql-proxy wait [flags] +``` + +### Options + +``` + -h, --help help for wait + -m, --max duration maximum amount of time to wait for startup (default 30s) +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy](cloud-sql-proxy.md) - cloud-sql-proxy authorizes and encrypts connections to Cloud SQL. + diff --git a/examples/disaster-recovery/README.md b/examples/disaster-recovery/README.md new file mode 100644 index 000000000..2ffc3f501 --- /dev/null +++ b/examples/disaster-recovery/README.md @@ -0,0 +1,51 @@ +# Coordinate disaster recovery with Secret Manager + +## Background + +This document assumes you are already using the following strategy for +detecting and triggering failovers: +1. Using an independent service to detect when the primary is down +2. Trigger a promotion of an existing read replica to become a primary +3. Update a Secret Manager secret with the name of the current primary + +## Restart Auth Proxy when secret changes + +This option uses a wrapper script around the Cloud SQL Auth Proxy to detect +when the secret has changed, and restart the Proxy with the new value. This +could be done in many languages, but here’s an example using bash: + +> [failover.sh](examples/disaster-recovery/failover.sh) +```sh +#! /bin/bash + +SECRET_ID="my-secret-id" # TODO(developer): replace this value +REFRESH_INTERVAL=5 +PORT=5432 + +# Get the latest version of the secret and start the Proxy +INSTANCE=$(gcloud secrets versions access "latest" --secret="$SECRET_ID") +cloud-sql-proxy --port "$PORT" "$INSTANCE" & +PID=$! + +# Every 5s, get the latest version of the secret. If it's changed, restart the +# Proxy with the new value. +while true; do + sleep $REFRESH_INTERVAL + NEW=$(gcloud secrets versions access "latest" --secret="$SECRET_ID") + if [ "$INSTANCE" != "$NEW" ]; then + INSTANCE=$NEW + kill $PID + wait $PID + cloud-sql-proxy --port "$PORT" "$INSTANCE" & + PID=$! + fi +done +``` + +## Benefits of this approach + +Using this approach will help assist with failovers without needing to +reconfigure your application. Instead, by changing the Proxy the application +will always connect to 127.0.0.1 and won’t need to restart to apply +configuration changes. Additionally, it will prevent split brain syndrome by +ensuring that your application can only connect to the current “primary”. diff --git a/examples/disaster-recovery/failover.sh b/examples/disaster-recovery/failover.sh new file mode 100644 index 000000000..66ce3c8f8 --- /dev/null +++ b/examples/disaster-recovery/failover.sh @@ -0,0 +1,40 @@ +# Copyright 2023 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_secret_manager_failover] +#! /bin/bash + +SECRET_ID="my-secret-id" # TODO(developer): replace this value +PORT=5432 + +# Get the latest version of the secret and start the proxy +INSTANCE=$(gcloud secrets versions access "latest" --secret="$SECRET_ID") +cloud-sql-proxy --port "$PORT" "$INSTANCE" & +PID=$! + +# Every 5s, get the latest version of the secret. If it's changed, restart the +# proxy with the new value. +while true; do + sleep 5 + NEW=$(gcloud secrets versions access "latest" --secret="$SECRET_ID") + if [ "$INSTANCE" != "$NEW" ]; then + INSTANCE=$NEW + kill $PID + wait $PID + cloud-sql-proxy --port "$PORT" "$INSTANCE" & + PID=$! + fi +done +# [END cloud_sql_proxy_secret_manager_failover] + diff --git a/examples/k8s-health-check/README.md b/examples/k8s-health-check/README.md index 2508f009f..99325f754 100644 --- a/examples/k8s-health-check/README.md +++ b/examples/k8s-health-check/README.md @@ -1,70 +1,194 @@ -# Cloud SQL proxy health checks +# Cloud SQL Auth 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. +Kubernetes supports [three types of health checks][k8s-docs]: -## Running Cloud SQL proxy with health checks in Kubernetes -1. Configure your Cloud SQL proxy container to include health check probes. +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. + +[k8s-docs]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + +When enabling the `--health-check` flag, the proxy will start an HTTP server on +localhost with three endpoints: + +- `/startup`: Returns 200 status when the proxy has finished starting up. +Otherwise returns 503 status. + +- `/liveness`: Always returns 200 status. If this endpoint is not responding, +the proxy is in a bad state and should be restarted. + +- `/readiness`: Returns 200 status when the proxy has started, has available + connections if max connections have been set with the `--max-connections` + flag, and when the proxy can connect to all registered instances. Otherwise, + returns a 503 status. + + +To configure the address, use `--http-address`. To configure the port, use +`--http-port`. + +## Running Cloud SQL Auth Proxy with health checks in Kubernetes +1. Configure your Cloud SQL Auth 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" - ``` +```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/ +startupProbe: + # We recommend adding a startup probe to the proxy sidecar + # container. This will ensure that service traffic will be routed to + # the pod only after the proxy has successfully started. + httpGet: + path: /startup + port: 9801 + periodSeconds: 1 + timeoutSeconds: 5 + failureThreshold: 20 +livenessProbe: + # We recommend adding a liveness probe to the proxy sidecar container. + httpGet: + path: /liveness + port: 9801 + # 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 +# We do not recommend adding a readiness probe under most circumstances +``` + +2. Enable the health checks by setting `--http-address` and `--http-port` (optional) to your + proxy container configuration under `command: `. + > [proxy_with_http_health_check.yaml](proxy_with_http_health_check.yaml#L53-L76) + +```yaml +args: +# Replace with the instance connection +# name in the format: "project_name:region:instance_name" +- + +env: +# It can be easier to manage the k8s configuration file when you +# use environment variables instead of CLI flags. This is the +# recommended configuration. This configuration is enabled by default +# when the cloud-sql-proxy-operator configures a proxy image + +# Replace with the port that the proxy should open +# to listen for database connections from the application +- name: CSQL_PROXY_PORT + value: + +# Enable HTTP healthchecks on port 9801. This enables /liveness, +# /readiness and /startup health check endpoints. Allow connections +# listen for connections on any interface (0.0.0.0) so that the +# k8s management components can reach these endpoints. +- name: CSQL_PROXY_HEALTH_CHECK + value: "true" +- name: CSQL_PROXY_HTTP_PORT + value: "9801" +- name: CSQL_PROXY_HTTP_ADDRESS + value: 0.0.0.0 +# Configure the proxy to exit gracefully when sent a k8s configuration +# file. +- name: CSQL_PROXY_EXIT_ZERO_ON_SIGTERM + value: "true" + +``` + +### Readiness Health Check Configuration + +For most common usage, adding a readiness healthcheck to the proxy sidecar +container is unnecessary. An improperly configured readiness check can degrade +the application's availability. + +The proxy readiness probe fails when (1) the proxy used all its available +concurrent connections to a database, (2) the network connection +to the database is interrupted, (3) the database server is unavailable due +to a maintenance operation. These are transient states that usually resolve +within a few seconds. + +Most applications are resilient to transient database connection failures, and +do not need to be restarted. We recommend adding a readiness check to the +application container instead of the proxy container. The application can be +programmed to report whether it is ready to receive requests, and the healthcheck +can be tuned to restart the pod when the application is permanently stuck. + +You should use the proxy container's readiness probe when these circumstances +should cause k8s to terminate the entire pod: + +- The proxy can't connect to the database instances. +- The max number of connections are in use. + +When you do use the proxy pod's readiness probe, be sure to set the +`failureThreshold` and `periodSeconds` to avoid restarting the pod on frequent +transient failures. + +### Readiness Health Check Examples + +The DBA team performs database fail-overs drills without notice. A +batch job should fail if it cannot connect the database for 3 minutes. +Set the readiness check so that the pod will be terminated after 3 minutes +of consecutive readiness check failures. (6 failed readiness checks taken every 30 +seconds, 6 x 30sec = 3 minutes.) + +```yaml +readinessProbe: + httpGet: + path: /readiness + port: 9801 + initialDelaySeconds: 30 + # 30 sec period x 6 failures = 3 min until the pod is terminated + periodSeconds: 30 + failureThreshold: 6 + timeoutSeconds: 10 + successThreshold: 1 +``` + +A web application has a database connection pool leak and the +engineering team can't find the root cause. To keep the system running, +the application should be automatically restarted if it consumes 50 connections +for more than 1 minute. + + +```yaml + containers: + - name: my-application + image: gcr.io/my-container/my-application:1.1 + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.21.1 + args: + # Set the --max-connections flag to 50 + - "--max-connections" + - "50" + - "--port=" + - "" +# ... + readinessProbe: + httpGet: + path: /readiness + port: 9801 + initialDelaySeconds: 10 + # 5 sec period x 12 failures = 60 sec until the pod is terminated + periodSeconds: 5 + failureThreshold: 12 + timeoutSeconds: 5 + successThreshold: 1 +``` + diff --git a/examples/k8s-health-check/proxy_with_http_health_check.yaml b/examples/k8s-health-check/proxy_with_http_health_check.yaml index d8ff78ad5..dc6b20f07 100644 --- a/examples/k8s-health-check/proxy_with_http_health_check.yaml +++ b/examples/k8s-health-check/proxy_with_http_health_check.yaml @@ -46,62 +46,109 @@ spec: secretKeyRef: name: key: database + # The proxy should be run as a native sidecar container, available in + # Kubernetes 1.29 and higher. This will ensure that the proxy container + # is ready before the main application container is started, and + # that the proxy container's exit status will not impact the pod's exit + # status. See the Kubernetes documentation: + # https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/ + initContainers: - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth 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" + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.4 + imagePullPolicy: IfNotPresent - # 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" + args: + # Replace with the instance connection + # name in the format: "project_name:region:instance_name" + - - # 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/ + env: + # Using environment variables instead of CLI arguments to configure the + # proxy k8s configuration can make it easier to read your + # k8s configuration files. + # + # This is the recommended configuration for your proxy pod template. + # It is used by the cloud-sql-proxy-operator when configuring the + # proxy container. + + # Replace with the port that the proxy should open + # to listen for database connections from the application + - name: CSQL_PROXY_PORT + value: + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - name: CSQL_PROXY_PRIVATE_IP + # value: "true" + + # Enable HTTP healthchecks on port 9801. This enables /liveness, + # /readiness and /startup health check endpoints. Allow connections + # listen for connections on any interface (0.0.0.0) so that the + # k8s management components can reach these endpoints. + - name: CSQL_PROXY_HEALTH_CHECK + value: "true" + - name: CSQL_PROXY_HTTP_PORT + value: "9801" + - name: CSQL_PROXY_HTTP_ADDRESS + value: 0.0.0.0 + + # Configure the proxy to exit gracefully when sent a k8s configuration + # file. + - name: CSQL_PROXY_EXIT_ZERO_ON_SIGTERM + value: "true" + + # Enable the admin api server (which only listens for local connections) + # and enable the /quitquitquit endpoint. This allows other pods + # to shut down the proxy gracefully when they are ready to exit. + - name: CSQL_PROXY_QUITQUITQUIT + value: "true" + - name: CSQL_PROXY_ADMIN_PORT + value: "9092" + + # Enable structured logging with LogEntry format + - name: CSQL_PROXY_STRUCTURED_LOGS + value: "true" + + # Configure kubernetes to call run the cloud-sql-proxy shutdown command + # before sending SIGTERM to the proxy when stopping the pod. This will + # give the proxy more time to gracefully exit. + lifecycle: + preStop: + exec: + command: ["/cloud-sql-proxy","shutdown", "--admin-port","9192"] + + # The /startup probe returns OK when the proxy is ready to receive + # connections from the application. In this example, k8s will check + # once a second for 60 seconds. + # + # We strongly recommend adding a startup probe to the proxy sidecar + # container. This will ensure that service traffic will be routed to + # the pod only after the proxy has successfully started. + startupProbe: + failureThreshold: 60 + httpGet: + path: /startup + port: 9801 + scheme: HTTP + periodSeconds: 1 + successThreshold: 1 + timeoutSeconds: 10 + # The /liveness probe returns OK as soon as the proxy application has + # begun its startup process and continues to return OK until the + # process stops. + # + # We recommend adding a liveness probe to the proxy sidecar container. livenessProbe: + failureThreshold: 3 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 + port: 9801 + scheme: HTTP + # The probe will be checked every 10 seconds. + periodSeconds: 10 # Number of times the probe is allowed to fail before the transition # from healthy to failure state. # @@ -110,25 +157,57 @@ spec: # 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 + successThreshold: 1 + # The probe will fail if it does not respond in 10 seconds + timeoutSeconds: 10 readinessProbe: + # The /readiness probe returns OK when the proxy can establish + # a new connections to its databases. + # + # Please use the readiness probe to the proxy sidecar with caution. + # An improperly configured readiness probe can cause unnecessary + # interruption to the application. See README.md for more detail. httpGet: path: /readiness - port: 8090 - initialDelaySeconds: 0 + port: 9801 + initialDelaySeconds: 10 periodSeconds: 10 - timeoutSeconds: 5 + timeoutSeconds: 10 # 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 + failureThreshold: 6 + + # Declare the HTTP Port so that k8s components can reach the + # metrics and health check endpoints. + ports: + - containerPort: 9801 + protocol: TCP + # 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" + securityContext: + # The default Cloud SQL Auth Proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + # Use a read-only filesystem + readOnlyRootFilesystem: true + # Do not allow privilege escalation + allowPrivilegeEscalation : false + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File volumes: - name: secret: diff --git a/examples/k8s-service/README.md b/examples/k8s-service/README.md index 525de08d6..3ab5ad0ec 100644 --- a/examples/k8s-service/README.md +++ b/examples/k8s-service/README.md @@ -169,15 +169,20 @@ For the PgBouncer deployment, we add the proxy as a sidecar, starting it on port > [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L70-L76) + ``` yaml +initContainers: - 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" + restartPolicy: Always + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.21.1 # make sure to use the latest version + args: + # Replace DB_PORT with the port the proxy should listen on + - "--port=" + - "" securityContext: runAsNonRoot: true ``` + Next, we create a PgBouncer service, listening on port 5342: diff --git a/examples/k8s-service/pgbouncer_deployment.yaml b/examples/k8s-service/pgbouncer_deployment.yaml index 5490ea600..cf4eb9df5 100644 --- a/examples/k8s-service/pgbouncer_deployment.yaml +++ b/examples/k8s-service/pgbouncer_deployment.yaml @@ -81,10 +81,14 @@ spec: value: "/etc/server/key.pem" - name: CLIENT_TLS_CERT_FILE value: "/etc/server/cert.pem" + initContainers: - 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" + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth Proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.4 + args: + - "--port=5431" + - "" securityContext: runAsNonRoot: true diff --git a/examples/k8s-sidecar/README.md b/examples/k8s-sidecar/README.md index faed91753..8cbde9ca9 100644 --- a/examples/k8s-sidecar/README.md +++ b/examples/k8s-sidecar/README.md @@ -1,6 +1,6 @@ -# Using the Cloud SQL proxy on Kubernetes +# Using the Cloud SQL Auth Proxy on Kubernetes -The Cloud SQL proxy is the recommended way to connect to Cloud SQL, even when +The Cloud SQL Auth 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. @@ -47,7 +47,7 @@ above, the values will be in the env vars `DB_USER`, `DB_PASS`, and `DB_NAME`. ## Setting up a service account -The first step to running the Cloud SQL proxy in Kubernetes is creating a +The first step to running the Cloud SQL Auth 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 @@ -70,7 +70,7 @@ The service account for your application needs to meet the following criteria: ## 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. +Cloud SQL Auth Proxy. There are two recommended ways to do this. ### Workload Identity @@ -131,8 +131,8 @@ bind a [Kubernetes Service Account (KSA)][ksa] to a Google Service Account ### 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. +to mount a service account key file into the Cloud SQL Auth Proxy pod and use the +`--credentials-file` flag. 1. Create a credential file for your service account key: ```sh @@ -158,7 +158,7 @@ to mount a service account key file into the Cloud SQL proxy pod and use the [k8s-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ -## Run the Cloud SQL proxy as a sidecar +## Run the Cloud SQL Auth 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 @@ -176,25 +176,26 @@ as a separate service for several reasons: accurately scope and request resources to match your applications as it scales -1. Add the Cloud SQL proxy to the pod configuration under `containers`: + +1. Add the Cloud SQL Auth Proxy to the pod configuration under `containers`: > [proxy_with_workload-identity.yaml](proxy_with_workload_identity.yaml#L39-L69) ```yaml + initContainers: - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth 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" - + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.21.1 # make sure to use the latest version + args: # 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" + # - "--private-ip" # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" + - "--port=" + - "" securityContext: - # The default Cloud SQL proxy image runs as the + # The default Cloud SQL Auth Proxy image runs as the # "nonroot" user and group (uid: 65532) by default. runAsNonRoot: true # Resource configuration depends on an application's requirements. You @@ -211,15 +212,17 @@ as a separate service for several reasons: # 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: + the `--credentials-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" + - "--credentials-file=/secrets/service_account.json" securityContext: - # The default Cloud SQL proxy image runs as the + # The default Cloud SQL Auth Proxy image runs as the # "nonroot" user and group (uid: 65532) by default. runAsNonRoot: true volumeMounts: @@ -232,7 +235,7 @@ as a separate service for several reasons: `` you specified in the command section. -## Connecting without the Cloud SQL proxy +## Connecting without the Cloud SQL Auth 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. diff --git a/examples/k8s-sidecar/job_with_shutdown_hook.yaml b/examples/k8s-sidecar/job_with_shutdown_hook.yaml new file mode 100644 index 000000000..6adb3d9de --- /dev/null +++ b/examples/k8s-sidecar/job_with_shutdown_hook.yaml @@ -0,0 +1,89 @@ +# Copyright 2023 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 demonstrates how to configure a batch job so that it shuts down +# the proxy containers when it has finished processing. +# +# The main job container should send a POST request to the proxy's /quitquitquit +# api when the job process finishes. This will cause the proxy side-car +# container to shut down. +# +# In Kubernetes 1.28, side-car containers will be properly supported, and this +# extra step will become unnecessary. +# +# See https://github.com/kubernetes/enhancements/issues/753 +# and https://github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/issues/381 + +apiVersion: batch/v1 +kind: Job +metadata: + name: job + labels: + app: busybox +spec: + template: + metadata: + creationTimestamp: null + labels: + app: busybox + spec: + containers: + - name: my-application + # Note: This demonstrates a way to run the proxy in an older + # kubernetes cluster that does not support native sidecar containers. + # It is better to run the job as a native sidecar container. + # + # See the Kubernetes documentation: + # https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/ + # + # Run your batch job command. + # Then, send a HTTTP POST request to the proxy sidecar container's + # /quitquitquit api. This will cause the proxy process to exit. + command: + - sh + - -c + - > + my_batch_job --host=127.0.0.1 --port= --username= --dbname= + curl http://localhost:9091/quitquitquit + image: busybox + imagePullPolicy: IfNotPresent + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + - name: cloud-sql-proxy + # It is recommended to use the latest version of the Cloud SQL Auth Proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1 + args: + # Enable the admin api server on port 9091 + - "--admin-port=9091" + # Enable the /quitquitquit admin api endpoint + - "--quitquitquit" + + # Tell the proxy to exit gracefully if it receives a SIGTERM + - "--exit-zero-on-sigterm" + + # Replace DB_PORT with the port the proxy should listen on + - "--port=" + - "" + + securityContext: + runAsNonRoot: true + resources: + requests: + memory: "2Gi" + cpu: "1" + restartPolicy: Never + terminationGracePeriodSeconds: 30 diff --git a/examples/k8s-sidecar/job_with_sidecar.yaml b/examples/k8s-sidecar/job_with_sidecar.yaml new file mode 100644 index 000000000..a081a3d2d --- /dev/null +++ b/examples/k8s-sidecar/job_with_sidecar.yaml @@ -0,0 +1,77 @@ +# Copyright 2025 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 demonstrates how to configure a batch job so that it shuts down +# the proxy containers when it has finished processing. +# +# This works in Kubernetes 1.29 and higher, demonstrating how to run the proxy +# using a native side-car container. +# +# See https://github.com/kubernetes/enhancements/issues/753 +# and https://github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/issues/381 + +apiVersion: batch/v1 +kind: Job +metadata: + name: job + labels: + app: busybox +spec: + template: + metadata: + creationTimestamp: null + labels: + app: busybox + spec: + containers: + - name: my-application + # Run your batch job command. + # Then, send a HTTTP POST request to the proxy sidecar container's + # /quitquitquit api. This will cause the proxy process to exit. + command: + - my_batch_job + - --host=127.0.0.1 + - --port= + - --username= + - --dbname= + image: my-application-image + imagePullPolicy: IfNotPresent + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + restartPolicy: Never + terminationGracePeriodSeconds: 30 + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth Proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.3 + args: + # Enable the admin api server on port 9091 + - "--admin-port=9091" + + # Tell the proxy to exit gracefully if it receives a SIGTERM + - "--exit-zero-on-sigterm" + + # Replace DB_PORT with the port the proxy should listen on + - "--port=" + - "" + securityContext: + runAsNonRoot: true + resources: + requests: + memory: "2Gi" + cpu: "1" diff --git a/examples/k8s-sidecar/proxy_with_sa_key.yaml b/examples/k8s-sidecar/proxy_with_sa_key.yaml index 2161278a2..47351c090 100644 --- a/examples/k8s-sidecar/proxy_with_sa_key.yaml +++ b/examples/k8s-sidecar/proxy_with_sa_key.yaml @@ -26,73 +26,74 @@ spec: 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" + - 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 + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth Proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1 + args: + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "--private-ip" - # 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" + # If you are not connecting with Automatic IAM AuthN, you can delete + # the following flag. + - "--auto-iam-authn" - # By default, the proxy will write all logs to stderr. In some - # environments, anything printed to stderr is considered an error. To - # disable this behavior and write all logs to stdout (except errors - # which will still go to stderr), use: - - "-log_debug_stdout" + # Enable structured logging with LogEntry format: + - "--structured-logs" - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" + # Replace DB_PORT with the port the proxy should listen on + - "--port=" + - "" - # [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_mount] + # This flag specifies where the service account key can be found + - "--credentials-file=/secrets/service_account.json" + securityContext: + # The default Cloud SQL Auth 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: + - 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 index b9eaa3ee9..7c68e7e68 100644 --- a/examples/k8s-sidecar/proxy_with_workload_identity.yaml +++ b/examples/k8s-sidecar/proxy_with_workload_identity.yaml @@ -30,63 +30,65 @@ spec: # [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" + - 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] + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + # It is recommended to use the latest version of the Cloud SQL Auth Proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1 + args: + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "--private-ip" - # 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" + # If you are not connecting with Automatic IAM, you can delete + # the following flag. + - "--auto-iam-authn" - # 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" + # Enable structured logging with LogEntry format: + - "--structured-logs" - # 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" + # Replace DB_PORT with the port the proxy should listen on + - "--port=" + - "" + + securityContext: + # The default Cloud SQL Auth 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/multi-container/ruby/Dockerfile b/examples/multi-container/ruby/Dockerfile new file mode 100644 index 000000000..a69f8e830 --- /dev/null +++ b/examples/multi-container/ruby/Dockerfile @@ -0,0 +1,28 @@ +# Copyright 2023 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. + + +FROM ruby:3.2.2 + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN bundle install + +EXPOSE 8080 + +CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "8080"] diff --git a/examples/multi-container/ruby/Gemfile b/examples/multi-container/ruby/Gemfile new file mode 100644 index 000000000..e1ff8e1e9 --- /dev/null +++ b/examples/multi-container/ruby/Gemfile @@ -0,0 +1,21 @@ +# Copyright 2023 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. + +source 'https://rubygems.org' + +gem 'json' +gem 'pg' +gem 'sequel' +gem 'sinatra' +gem 'thin' diff --git a/examples/multi-container/ruby/README.md b/examples/multi-container/ruby/README.md new file mode 100644 index 000000000..460fbf066 --- /dev/null +++ b/examples/multi-container/ruby/README.md @@ -0,0 +1,134 @@ +# Cloud SQL Auth Proxy Sidecar + +In the following example, we will deploy the Cloud SQL Proxy as a sidecar to an +existing application which connects to a Cloud SQL instance. + +## Before you begin + +1. If you haven't already, [create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project). + +1. [Enable the APIs](https://console.cloud.google.com/flows/enableapi?apiid=run.googleapis.com,sqladmin.googleapis.com,run.googleapis.com) that will be used during this tutorial: + + * Cloud SQL Admin + * Cloud Build + * Cloud Run + +1. Create a Cloud SQL Postgres Instance by following these +[instructions](https://cloud.google.com/sql/docs/postgres/create-instance). +Note the connection string and default password that you create. + +1. Create a database for your application by following these +[instructions](https://cloud.google.com/sql/docs/postgres/create-manage-databases). +Note the database name. + +1. Optionally, create a database user for your instance following these +[instructions](https://cloud.google.com/sql/docs/postgres/create-manage-users). +Otherwise, use the username "postgres". + +## Deploying the Application + +The application you will be deploying should connect to the Cloud SQL Proxy using +TCP mode (for example, using the address "127.0.0.1:5432"). Follow the examples +on the [Connect Auth Proxy documentation](https://cloud.google.com/sql/docs/postgres/connect-auth-proxy#expandable-1) +page to correctly configure your application. + +The connection pool is configured in the following sample: + +```ruby +require 'sinatra' +require 'sequel' + +set :bind, '0.0.0.0' +set :port, 8080 + +# Configure a connection pool that connects to the proxy via TCP +def connect_tcp + Sequel.connect( + adapter: 'postgres', + host: ENV["INSTANCE_HOST"], + port: ENV["DB_PORT"], + database: ENV["DB_NAME"], + user: ENV["DB_USER"], + password: ENV["DB_PASS"], + pool_timeout: 5, + max_connections: 5, + ) +end + +DB = connect_tcp() +``` + +Next, build the container image for the main application and deploy it: + +```bash +gcloud builds submit --tag gcr.io//run-cloudsql +``` + +Finally, update the `multicontainer.yaml` file with the correct values for your +deployment for `YOUR_PROJECT_ID`, `DB_USER`, `DB_PASS`, `DB_NAME`, and `INSTANCE_CONNECTION_NAME` +listing the Cloud SQL container image as a sidecar: + +```yaml +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + annotations: + run.googleapis.com/launch-stage: ALPHA + name: multicontainer-service +spec: + template: + metadata: + annotations: + run.googleapis.com/execution-environment: gen1 #or gen2 + + spec: + containers: + - name: my-app + image: gcr.io//run-cloudsql + ports: + - containerPort: 8080 + env: + - name: DB_USER + value: + - name: DB_PASS + value: + - name: DB_NAME + value: + - name: INSTANCE_HOST + value: "127.0.0.1" + - name: DB_PORT + value: "5432" + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest + args: + # Ensure the port number on the --port argument matches the value of + # the DB_PORT env var on the my-app container. + - "--port=5432" + - "" + +``` + +You can optionally use Secret Manager to store the database password. See +[this documentation](https://cloud.google.com/run/docs/deploying#yaml) for more details. + +Before deploying, you will need to make sure that the service account associated +with the Cloud Run deployment has the Cloud SQL Client role. +See [this documentation](https://cloud.google.com/sql/docs/postgres/roles-and-permissions) +for more details. The default service account will already have these permissions. + +Finally, you can deploy the service using: + +```bash +gcloud run services replace multicontainer.yaml +``` + +Once the service is deployed, the console should print out a URL. You can test +the service by sending a curl request with your gcloud identity token in the headers: + +```bash +curl -H \ +"Authorization: Bearer $(gcloud auth print-identity-token)" \ + +``` diff --git a/examples/multi-container/ruby/app.rb b/examples/multi-container/ruby/app.rb new file mode 100644 index 000000000..5d7757a9f --- /dev/null +++ b/examples/multi-container/ruby/app.rb @@ -0,0 +1,44 @@ +# Copyright 2023 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. + +require 'json' +require 'sequel' +require 'sinatra' + + +set :bind, '0.0.0.0' +set :port, 8080 + +# Configure a connection pool that connects to the proxy via TCP +def connect_tcp + Sequel.connect( + adapter: :postgres, + host: ENV.fetch("INSTANCE_HOST") { "127.0.0.1" }, + port: ENV.fetch("DB_PORT") { 5432 }, + database: ENV["DB_NAME"], + user: ENV["DB_USER"], + password: ENV["DB_PASS"], + pool_timeout: 5, + max_connections: 5, + ) +end + +DB = connect_tcp() + + +get '/' do + content_type :json + # Connect to the database and get the current time + return DB["SELECT NOW()"].all.to_json +end diff --git a/examples/multi-container/ruby/config.ru b/examples/multi-container/ruby/config.ru new file mode 100644 index 000000000..427d34717 --- /dev/null +++ b/examples/multi-container/ruby/config.ru @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +require './app' + +run Sinatra::Application diff --git a/examples/multi-container/ruby/multicontainer.yaml b/examples/multi-container/ruby/multicontainer.yaml new file mode 100644 index 000000000..68c2b005e --- /dev/null +++ b/examples/multi-container/ruby/multicontainer.yaml @@ -0,0 +1,58 @@ +# Copyright 2023 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: serving.knative.dev/v1 +kind: Service +metadata: + annotations: + run.googleapis.com/launch-stage: ALPHA + name: multicontainer-service +spec: + template: + metadata: + annotations: + run.googleapis.com/execution-environment: gen1 #or gen2 + # Uncomment the following line if connecting to Cloud SQL using Private IP + # via a VPC access connector + # run.googleapis.com/vpc-access-connector: + spec: + containers: + - name: my-app + image: gcr.io//run-cloudsql:latest + ports: + - containerPort: 8080 + env: + - name: DB_USER + value: + - name: DB_PASS + value: + - name: DB_NAME + value: + - name: INSTANCE_HOST + value: "127.0.0.1" + - name: DB_PORT + value: "5432" + initContainers: + - name: cloud-sql-proxy + restartPolicy: Always + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest + args: + # If connecting to a Cloud SQL instance within a VPC network, you can use the + # following flag to have the proxy connect over private IP + # - "--private-ip" + + # Ensure the port number on the --port argument matches the value of the DB_PORT env var on the my-app container. + - "--port=5432" + # instance connection name takes format "PROJECT:REGION:INSTANCE_NAME" + - "" diff --git a/go.mod b/go.mod index 18ee86189..1847d776f 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,87 @@ -module github.com/GoogleCloudPlatform/cloudsql-proxy +module github.com/GoogleCloudPlatform/cloud-sql-proxy/v2 -go 1.16 +go 1.25.7 require ( - cloud.google.com/go/compute v1.9.0 - github.com/coreos/go-systemd/v22 v22.3.2 - github.com/denisenkom/go-mssqldb v0.12.2 - github.com/go-sql-driver/mysql v1.6.0 - github.com/hanwen/go-fuse/v2 v2.1.0 - github.com/jackc/pgx/v4 v4.17.0 - github.com/lib/pq v1.10.6 - go.uber.org/zap v1.22.0 - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e - golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 - golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 - golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 - google.golang.org/api v0.93.0 + cloud.google.com/go/cloudsqlconn v1.20.1 + contrib.go.opencensus.io/exporter/prometheus v0.4.2 + contrib.go.opencensus.io/exporter/stackdriver v0.13.14 + github.com/coreos/go-systemd/v22 v22.7.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/google/go-cmp v0.7.0 + github.com/hanwen/go-fuse/v2 v2.9.0 + github.com/jackc/pgx/v5 v5.8.0 + github.com/microsoft/go-mssqldb v1.9.8 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + go.opencensus.io v0.24.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sys v0.42.0 + google.golang.org/api v0.271.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/trace v1.11.7 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/aws/aws-sdk-go v1.55.8 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/prometheus/prometheus v0.310.0 // indirect + github.com/prometheus/statsd_exporter v0.29.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/grpc v1.79.2 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 0da89d0e0..4c8244266 100644 --- a/go.sum +++ b/go.sum @@ -13,40 +13,24 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV 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/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 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/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.9.0 h1:ED/FP4xv8GJw63v556/ASNc1CeeLUO2Bs8nzaHchkHg= -cloud.google.com/go/compute v1.9.0/go.mod h1:lWv1h/zUWTm/LozzfTJhBSkd6ShQq8la8VeeuOEGxfY= +cloud.google.com/go/cloudsqlconn v1.20.1 h1:9X8MC34DsZLh/fNQiSWtLb43S6IjizQcfLRkcrKYxm0= +cloud.google.com/go/cloudsqlconn v1.20.1/go.mod h1:jnc/uIpkO46iTwk3YYlNIsIyIGszG5o6h8+IGDLPJ0U= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= 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= @@ -56,80 +40,115 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo 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= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= +contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= +contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpXy/0+JIb1M8KjigCJzx7+4= +contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc= 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= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= 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/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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/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-20211001041855-01bcc9b48dfe/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-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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= -github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 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/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/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 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/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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= @@ -137,8 +156,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt 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= @@ -154,10 +171,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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= @@ -167,18 +183,15 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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= @@ -186,188 +199,216 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf 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/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= 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/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -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/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= +github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= 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/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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= -github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 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.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= -github.com/jackc/pgproto3/v2 v2.3.1/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.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= -github.com/jackc/pgtype v1.12.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.17.0 h1:Hsx+baY8/zU2WtPLQyZi8WbecgcsWEeyoK1jvg/WgIo= -github.com/jackc/pgx/v4 v4.17.0/go.mod h1:Gd6RmOhtFLTu8cp/Fhq4kP195KrshxYJH3oW8AWJ1pw= -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/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 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/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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.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/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microsoft/go-mssqldb v1.9.8 h1:d4IFMvF/o+HdpXUqbBfzHvn/NlFA75YGcfHUUvDFJEM= +github.com/microsoft/go-mssqldb v1.9.8/go.mod h1:eGSRSGAW4hKMy5YcAenhCDjIRm2rhqIdmmwgciMzLus= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/prometheus v0.310.0 h1:iS0Uul/dHjy8ifBnqo3YEOhRxlTOWantRoDWwmIowwA= +github.com/prometheus/prometheus v0.310.0/go.mod h1:rs6XoWKvgAStqxHxb2Twh1BR6rp7qw7fmUgW+gaXjbw= +github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= +github.com/prometheus/statsd_exporter v0.29.0 h1:2iaiBLpAg/WcdW4nF0UTQfjCE2DXD+1NVnCM1ulPKd8= +github.com/prometheus/statsd_exporter v0.29.0/go.mod h1:3t+fT5ESGc0KBUfadCp43KOnVPFmbAF4c58Osotz3Zk= 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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -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/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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.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.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= -go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= 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= @@ -390,8 +431,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl 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= @@ -400,11 +439,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB 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-20181114220301-adae6a3d119a/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= @@ -412,10 +449,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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= @@ -430,48 +467,21 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ 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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 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-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/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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= @@ -480,16 +490,15 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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-20181116152217-5ac8a444bdc5/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= @@ -497,13 +506,11 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w 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-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-20200106162015-b016eb3dc98e/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= @@ -515,44 +522,21 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/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-20210124154548-22da62e12c0c/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-20210603081109-ebe580a85c40/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-20220128215802-99c3d69c2c27/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-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U= -golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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= @@ -560,16 +544,15 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= 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-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= 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= @@ -577,18 +560,14 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 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-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-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= @@ -596,7 +575,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn 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= @@ -615,27 +593,12 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY 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.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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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= @@ -652,40 +615,14 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.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.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.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -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/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0 h1:T2xt9gi0gHdxdnRkVQhT8mIvPaXKNsDNWz+L696M66M= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= 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= @@ -709,65 +646,18 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG 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-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -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-20220126215142-9970aeb2e350/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-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 h1:NX3L5YesD5qgxxrPHdKqHH38Ao0AG6poRXG+JljPsGU= -google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c h1:ZhFDeBMmFc/4g8/GwxnJ4rzB3O4GwQVNr+8Mh7Y5z4g= +google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c/go.mod h1:hf4r/rBuzaTkLUWRO03771Xvcs6P5hwdQK3UUEJjqo0= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -780,28 +670,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa 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/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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= @@ -814,20 +685,28 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/gcloud/gcloud.go b/internal/gcloud/gcloud.go new file mode 100644 index 000000000..612f9c16f --- /dev/null +++ b/internal/gcloud/gcloud.go @@ -0,0 +1,87 @@ +// 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 gcloud + +import ( + "bytes" + "encoding/json" + "fmt" + "runtime" + "time" + + "golang.org/x/oauth2" + exec "golang.org/x/sys/execabs" +) + +// config represents the credentials returned by `gcloud config config-helper`. +type config struct { + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry time.Time `json:"token_expiry"` + } +} + +func (c *config) Token() *oauth2.Token { + return &oauth2.Token{ + AccessToken: c.Credential.AccessToken, + Expiry: c.Credential.TokenExpiry, + } +} + +// Path returns the absolute path to the gcloud command. If the command is not +// found it returns an error. +func Path() (string, error) { + g := "gcloud" + if runtime.GOOS == "windows" { + g = g + ".cmd" + } + return exec.LookPath(g) +} + +// configHelper implements oauth2.TokenSource via the `gcloud config config-helper` command. +type configHelper struct{} + +// Token helps gcloudTokenSource implement oauth2.TokenSource. +func (configHelper) Token() (*oauth2.Token, error) { + gcloudCmd, err := Path() + if err != nil { + return nil, err + } + 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) + return nil, err + } + + c := &config{} + if err := json.Unmarshal(buf.Bytes(), c); err != nil { + return nil, err + } + return c.Token(), nil +} + +// TokenSource returns an oauth2.TokenSource backed by the gcloud CLI. +func TokenSource() (oauth2.TokenSource, error) { + h := configHelper{} + tok, err := h.Token() + if err != nil { + return nil, err + } + return oauth2.ReuseTokenSource(tok, h), nil +} diff --git a/proxy/fuse/fuse_openbsd.go b/internal/gcloud/gcloud_test.go similarity index 53% rename from proxy/fuse/fuse_openbsd.go rename to internal/gcloud/gcloud_test.go index 3db7628c1..9ef33aec3 100644 --- a/proxy/fuse/fuse_openbsd.go +++ b/internal/gcloud/gcloud_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 Google Inc. All Rights Reserved. +// 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. @@ -12,21 +12,28 @@ // 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 +package gcloud_test import ( - "errors" - "io" + "testing" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/gcloud" ) -func Supported() bool { - return false -} +func TestGcloud(t *testing.T) { + if testing.Short() { + t.Skip("skipping gcloud integration tests") + } + + // gcloud is configured. Try to obtain a token from gcloud config + // helper. + ts, err := gcloud.TokenSource() + if err != nil { + t.Fatalf("failed to get token source: %v", err) + } -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") + _, err = ts.Token() + if err != nil { + t.Fatalf("failed to get token: %v", err) + } } diff --git a/internal/healthcheck/healthcheck.go b/internal/healthcheck/healthcheck.go new file mode 100644 index 000000000..3c95d5409 --- /dev/null +++ b/internal/healthcheck/healthcheck.go @@ -0,0 +1,119 @@ +// 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 healthcheck tests and communicates the health of the Cloud SQL Auth Proxy. +package healthcheck + +import ( + "errors" + "fmt" + "net/http" + "sync" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" +) + +// Check provides HTTP handlers for use as healthchecks typically in a +// Kubernetes context. +type Check struct { + startedOnce *sync.Once + started chan struct{} + stoppedOnce *sync.Once + stopped chan struct{} + proxy *proxy.Client + logger cloudsql.Logger +} + +// NewCheck is the initializer for Check. +func NewCheck(p *proxy.Client, l cloudsql.Logger) *Check { + return &Check{ + startedOnce: &sync.Once{}, + started: make(chan struct{}), + stoppedOnce: &sync.Once{}, + stopped: make(chan struct{}), + proxy: p, + logger: l, + } +} + +// NotifyStarted notifies the check that the proxy has started up successfully. +func (c *Check) NotifyStarted() { + c.startedOnce.Do(func() { close(c.started) }) +} + +// NotifyStopped notifies the check that the proxy has started up successfully. +func (c *Check) NotifyStopped() { + c.stoppedOnce.Do(func() { close(c.stopped) }) +} + +// HandleStartup reports whether the Check has been notified of startup. +func (c *Check) HandleStartup(w http.ResponseWriter, _ *http.Request) { + select { + case <-c.started: + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + default: + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("error")) + } +} + +var ( + errNotStarted = errors.New("proxy is not started") + errStopped = errors.New("proxy has stopped") +) + +// HandleReadiness ensures the Check has been notified of successful startup, +// that the proxy has not reached maximum connections, and that the Proxy has +// not started shutting down. +func (c *Check) HandleReadiness(w http.ResponseWriter, _ *http.Request) { + select { + case <-c.started: + default: + c.logger.Errorf("[Health Check] Readiness failed: %v", errNotStarted) + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(errNotStarted.Error())) + return + } + + select { + case <-c.stopped: + c.logger.Errorf("[Health Check] Readiness failed: %v", errStopped) + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(errStopped.Error())) + return + default: + } + + if open, maxCount := c.proxy.ConnCount(); maxCount > 0 && open == maxCount { + err := fmt.Errorf("max connections reached (open = %v, max = %v)", open, maxCount) + c.logger.Errorf("[Health Check] Readiness failed: %v", err) + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(err.Error())) + return + } + + // No error cases apply, 200 status. + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} + +// HandleLiveness indicates the process is up and responding to HTTP requests. +// If this check fails (because it's not reachable), the process is in a bad +// state and should be restarted. +func (c *Check) HandleLiveness(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} diff --git a/internal/healthcheck/healthcheck_test.go b/internal/healthcheck/healthcheck_test.go new file mode 100644 index 000000000..bb86b72ea --- /dev/null +++ b/internal/healthcheck/healthcheck_test.go @@ -0,0 +1,224 @@ +// 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 healthcheck_test + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/healthcheck" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" +) + +var ( + logger = log.NewStdLogger(os.Stdout, os.Stdout) + proxyHost = "127.0.0.1" + proxyPort = 9000 +) + +func proxyAddr() string { + return fmt.Sprintf("%s:%d", proxyHost, proxyPort) +} + +func dialTCP(t *testing.T, addr string) net.Conn { + for i := 0; i < 10; i++ { + conn, err := net.Dial("tcp", addr) + if err == nil { + return conn + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("failed to dial %v", addr) + return nil +} + +type fakeDialer struct{} + +func (*fakeDialer) Dial(_ context.Context, _ string, _ ...cloudsqlconn.DialOption) (net.Conn, error) { + conn, _ := net.Pipe() + return conn, nil +} + +func (*fakeDialer) EngineVersion(_ context.Context, _ string) (string, error) { + return "POSTGRES_14", nil +} + +func (*fakeDialer) Close() error { + return nil +} + +func newProxyWithParams(t *testing.T, maxConns uint64, dialer cloudsql.Dialer, instances []proxy.InstanceConnConfig) *proxy.Client { + c := &proxy.Config{ + Addr: proxyHost, + Port: proxyPort, + Instances: instances, + MaxConnections: maxConns, + } + p, err := proxy.NewClient(context.Background(), dialer, logger, c, nil) + if err != nil { + t.Fatalf("proxy.NewClient: %v", err) + } + return p +} + +func newTestProxyWithMaxConns(t *testing.T, maxConns uint64) *proxy.Client { + return newProxyWithParams(t, maxConns, &fakeDialer{}, []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }) +} + +func newTestProxy(t *testing.T) *proxy.Client { + return newProxyWithParams(t, 0, &fakeDialer{}, []proxy.InstanceConnConfig{{Name: "proj:region:pg"}}) +} + +func TestHandleStartupWhenNotNotified(t *testing.T) { + p := newTestProxy(t) + defer func() { + if err := p.Close(); err != nil { + t.Logf("failed to close proxy client: %v", err) + } + }() + check := healthcheck.NewCheck(p, logger) + + rec := httptest.NewRecorder() + check.HandleStartup(rec, &http.Request{URL: &url.URL{}}) + + // Startup is not complete because the Check has not been notified of the + // proxy's startup. + resp := rec.Result() + if got, want := resp.StatusCode, http.StatusServiceUnavailable; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} + +func TestHandleStartupWhenNotified(t *testing.T) { + p := newTestProxy(t) + defer func() { + if err := p.Close(); err != nil { + t.Logf("failed to close proxy client: %v", err) + } + }() + check := healthcheck.NewCheck(p, logger) + + check.NotifyStarted() + + rec := httptest.NewRecorder() + check.HandleStartup(rec, &http.Request{URL: &url.URL{}}) + + resp := rec.Result() + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} + +func TestHandleReadinessWhenNotNotified(t *testing.T) { + p := newTestProxy(t) + defer func() { + if err := p.Close(); err != nil { + t.Logf("failed to close proxy client: %v", err) + } + }() + check := healthcheck.NewCheck(p, logger) + + rec := httptest.NewRecorder() + check.HandleReadiness(rec, &http.Request{URL: &url.URL{}}) + + resp := rec.Result() + if got, want := resp.StatusCode, http.StatusServiceUnavailable; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} + +func TestHandleReadinessWhenStopped(t *testing.T) { + p := newTestProxy(t) + defer func() { + if err := p.Close(); err != nil { + t.Logf("failed to close proxy client: %v", err) + } + }() + check := healthcheck.NewCheck(p, logger) + + check.NotifyStarted() // The Proxy has started. + check.NotifyStopped() // And now the Proxy is shutting down. + + rec := httptest.NewRecorder() + check.HandleReadiness(rec, &http.Request{URL: &url.URL{}}) + + resp := rec.Result() + if got, want := resp.StatusCode, http.StatusServiceUnavailable; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} + +func TestHandleReadinessForMaxConns(t *testing.T) { + p := newTestProxyWithMaxConns(t, 1) + defer func() { + if err := p.Close(); err != nil { + t.Logf("failed to close proxy client: %v", err) + } + }() + started := make(chan struct{}) + check := healthcheck.NewCheck(p, logger) + go p.Serve(context.Background(), func() { + check.NotifyStarted() + close(started) + }) + select { + case <-started: + // proxy has started + case <-time.After(10 * time.Second): + t.Fatal("proxy has not started after 10 seconds") + } + + conn := dialTCP(t, proxyAddr()) + defer conn.Close() + + // The proxy calls the dialer in a separate goroutine. So wait for that + // goroutine to run before asserting on the readiness response. + waitForConnect := func(t *testing.T, wantCode int) *http.Response { + for i := 0; i < 10; i++ { + rec := httptest.NewRecorder() + check.HandleReadiness(rec, &http.Request{URL: &url.URL{}}) + resp := rec.Result() + if resp.StatusCode == wantCode { + return resp + } + time.Sleep(time.Second) + } + t.Fatalf("failed to receive status code = %v", wantCode) + return nil + } + resp := waitForConnect(t, http.StatusServiceUnavailable) + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read response body: %v", err) + } + if !strings.Contains(string(body), "max connections") { + t.Fatalf("want max connections error, got = %v", string(body)) + } +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 000000000..295ae20a5 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,124 @@ +// 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 log //nolint:revive + +import ( + "fmt" + "io" + llog "log" + "log/slog" + "os" + "time" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" +) + +// StdLogger is the standard logger that distinguishes between info and error +// logs. +type StdLogger struct { + stdLog *llog.Logger + errLog *llog.Logger +} + +// NewStdLogger create a Logger that uses out and err for informational and +// error messages. +func NewStdLogger(out, err io.Writer) cloudsql.Logger { + return &StdLogger{ + stdLog: llog.New(out, "", llog.LstdFlags), + errLog: llog.New(err, "", llog.LstdFlags), + } +} + +// Infof logs informational messages +func (l *StdLogger) Infof(format string, v ...interface{}) { + l.stdLog.Printf(format, v...) +} + +// Errorf logs error messages +func (l *StdLogger) Errorf(format string, v ...interface{}) { + l.errLog.Printf(format, v...) +} + +// Debugf logs debug messages +func (l *StdLogger) Debugf(format string, v ...interface{}) { + l.stdLog.Printf(format, v...) +} + +// StructuredLogger writes log messages in JSON. +type StructuredLogger struct { + stdLog *slog.Logger + errLog *slog.Logger +} + +// Infof logs informational messages +func (l *StructuredLogger) Infof(format string, v ...interface{}) { + l.stdLog.Info(fmt.Sprintf(format, v...)) +} + +// Errorf logs error messages +func (l *StructuredLogger) Errorf(format string, v ...interface{}) { + l.errLog.Error(fmt.Sprintf(format, v...)) +} + +// Debugf logs debug messages +func (l *StructuredLogger) Debugf(format string, v ...interface{}) { + l.stdLog.Debug(fmt.Sprintf(format, v...)) +} + +// NewStructuredLogger creates a Logger that logs messages using JSON. +func NewStructuredLogger(quiet bool) cloudsql.Logger { + var infoHandler, errorHandler slog.Handler + if quiet { + infoHandler = slog.DiscardHandler + } else { + infoHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: replaceAttr, + }) + } + errorHandler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelError, + ReplaceAttr: replaceAttr, + }) + + l := &StructuredLogger{ + stdLog: slog.New(infoHandler), + errLog: slog.New(errorHandler), + } + return l +} + +// replaceAttr remaps default Go logging keys to adhere to LogEntry format +// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry +func replaceAttr(groups []string, a slog.Attr) slog.Attr { + if groups != nil { + return a + } + + switch a.Key { + case slog.LevelKey: + a.Key = "severity" + case slog.MessageKey: + a.Key = "message" + case slog.SourceKey: + a.Key = "sourceLocation" + case slog.TimeKey: + a.Key = "timestamp" + if a.Value.Kind() == slog.KindTime { + a.Value = slog.StringValue(a.Value.Time().Format(time.RFC3339)) + } + } + return a +} diff --git a/internal/proxy/fuse.go b/internal/proxy/fuse.go new file mode 100644 index 000000000..da9694fc8 --- /dev/null +++ b/internal/proxy/fuse.go @@ -0,0 +1,93 @@ +// 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. + +//go:build !windows && !openbsd && !freebsd + +package proxy + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" +) + +// symlink implements a symbolic link, returning the underlying path when +// Readlink is called. +type symlink struct { + fs.Inode + path string +} + +// Readlink implements fs.NodeReadlinker and returns the symlink's path. +func (s *symlink) Readlink(_ context.Context) ([]byte, syscall.Errno) { + return []byte(s.path), fs.OK +} + +// readme represents a static read-only text file. +type readme struct { + fs.Inode +} + +const readmeText = ` +When applications attempt to open files in this directory, a remote connection +to the Cloud SQL instance of the same name will be established. + +For example, when you run one of the following commands, the proxy will initiate +a connection to the corresponding Cloud SQL instance, given you have the correct +IAM permissions. + + mysql -u root -S "/somedir/project:region:instance" + + # or + + psql "host=/somedir/project:region:instance dbname=mydb user=myuser" + +For MySQL, the proxy will create a socket with the instance connection name +(e.g., project:region:instance) in this directory. For Postgres, the proxy will +create a directory with the instance connection name, and create a socket inside +that directory with the special Postgres name: .s.PGSQL.5432. + +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(_ context.Context, _ 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(_ context.Context, _ 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(_ context.Context, _ 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/internal/proxy/fuse_darwin.go similarity index 73% rename from proxy/fuse/fuse_darwin.go rename to internal/proxy/fuse_darwin.go index 19ff5cba2..8cefe7ce4 100644 --- a/proxy/fuse/fuse_darwin.go +++ b/internal/proxy/fuse_darwin.go @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// 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. @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fuse +package proxy import ( + "errors" "os" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" ) const ( @@ -25,9 +24,9 @@ const ( 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 { +// SupportsFUSE checks if macfuse or osxfuse are installed on the host by +// looking for both in their known installation location. +func SupportsFUSE() error { // 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. @@ -35,9 +34,8 @@ func Supported() bool { 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 errors.New("failed to find osxfuse or macfuse: verify FUSE installation and try again (see https://osxfuse.github.io)") } } - return true + return nil } diff --git a/internal/proxy/fuse_freebsd.go b/internal/proxy/fuse_freebsd.go new file mode 100644 index 000000000..c675a8e7b --- /dev/null +++ b/internal/proxy/fuse_freebsd.go @@ -0,0 +1,38 @@ +// 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 + +import ( + "context" + "errors" +) + +var errFUSENotSupported = errors.New("FUSE is not supported on FreeBSD") + +// SupportsFUSE is false on FreeBSD. +func SupportsFUSE() error { + return errFUSENotSupported +} + +type fuseMount struct { + // fuseDir is always an empty string on FreeBSD. + fuseDir string +} + +func configureFUSE(c *Client, conf *Config) (*Client, error) { return nil, errFUSENotSupported } +func (c *Client) fuseMounts() []*socketMount { return nil } +func (c *Client) serveFuse(ctx context.Context, notify func()) error { return errFUSENotSupported } +func (c *Client) unmountFUSE() error { return nil } +func (c *Client) waitForFUSEMounts() {} diff --git a/proxy/fuse/fuse_linux.go b/internal/proxy/fuse_linux.go similarity index 73% rename from proxy/fuse/fuse_linux.go rename to internal/proxy/fuse_linux.go index 4bd1dde67..264e2acf2 100644 --- a/proxy/fuse/fuse_linux.go +++ b/internal/proxy/fuse_linux.go @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// 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. @@ -12,23 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fuse +package proxy import ( + "errors" "os/exec" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" ) -// Supported returns true if the current system supports FUSE. -func Supported() bool { +// SupportsFUSE checks if the fusermount binary is present in the PATH or a well +// known location. +func SupportsFUSE() error { // 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 errors.New("fusermount binary not found in PATH or /bin") } } - return true + return nil } diff --git a/proxy/fuse/fuse_linux_test.go b/internal/proxy/fuse_linux_test.go similarity index 72% rename from proxy/fuse/fuse_linux_test.go rename to internal/proxy/fuse_linux_test.go index 6371c2a6c..b8ad06ea2 100644 --- a/proxy/fuse/fuse_linux_test.go +++ b/internal/proxy/fuse_linux_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// 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. @@ -12,16 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build linux -// +build linux - -package fuse_test +package proxy_test import ( "os" "testing" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/fuse" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" ) func TestFUSESupport(t *testing.T) { @@ -34,14 +31,13 @@ func TestFUSESupport(t *testing.T) { os.Unsetenv("PATH") return func() { os.Setenv("PATH", original) } } - if !fuse.Supported() { - t.Fatal("expected FUSE to be supported") + if err := proxy.SupportsFUSE(); err != nil { + t.Fatalf("expected FUSE to be support (PATH set): %v", err) } cleanup := removePath() defer cleanup() - if !fuse.Supported() { - t.Fatal("expected FUSE to be supported") + if err := proxy.SupportsFUSE(); err != nil { + t.Fatalf("expected FUSE to be supported (PATH unset): %v", err) } - } diff --git a/internal/proxy/fuse_openbsd.go b/internal/proxy/fuse_openbsd.go new file mode 100644 index 000000000..cebf80c92 --- /dev/null +++ b/internal/proxy/fuse_openbsd.go @@ -0,0 +1,38 @@ +// 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 + +import ( + "context" + "errors" +) + +var errFUSENotSupported = errors.New("FUSE is not supported on OpenBSD") + +// SupportsFUSE is false on OpenBSD. +func SupportsFUSE() error { + return errFUSENotSupported +} + +type fuseMount struct { + // fuseDir is always an empty string on OpenBSD. + fuseDir string +} + +func configureFUSE(c *Client, conf *Config) (*Client, error) { return nil, errFUSENotSupported } +func (c *Client) fuseMounts() []*socketMount { return nil } +func (c *Client) serveFuse(ctx context.Context, notify func()) error { return errFUSENotSupported } +func (c *Client) unmountFUSE() error { return nil } +func (c *Client) waitForFUSEMounts() {} diff --git a/internal/proxy/fuse_test.go b/internal/proxy/fuse_test.go new file mode 100644 index 000000000..02face315 --- /dev/null +++ b/internal/proxy/fuse_test.go @@ -0,0 +1,430 @@ +// 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. + +//go:build !windows && !darwin + +package proxy_test + +import ( + "context" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" + "github.com/hanwen/go-fuse/v2/fs" +) + +func randTmpDir(t interface { + Fatalf(format string, args ...interface{}) +}) string { + name, err := os.MkdirTemp("", "*") + if err != nil { + t.Fatalf("failed to create tmp dir: %v", err) + } + return name +} + +// newTestClient is a convenience function for testing that creates a +// proxy.Client and starts it. The returned cleanup function is also a +// convenience. Callers may choose to ignore it and manually close the client. +func newTestClient(t *testing.T, d cloudsql.Dialer, fuseDir, fuseTempDir string) (*proxy.Client, chan error, func()) { + conf := &proxy.Config{FUSEDir: fuseDir, FUSETempDir: fuseTempDir} + + // This context is only used to call the Cloud SQL API + c, err := proxy.NewClient(context.Background(), d, testLogger, conf, nil) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + ready := make(chan struct{}) + servErrCh := make(chan error) + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + + servErr := c.Serve(ctx, func() { close(ready) }) + select { + case servErrCh <- servErr: + case <-ctx.Done(): + } + }() + select { + case <-ready: + case <-time.Tick(5 * time.Second): + t.Fatal("failed to Serve") + } + return c, servErrCh, func() { + if cErr := c.Close(); cErr != nil { + t.Logf("failed to close client: %v", cErr) + } + cancel() + } +} + +func TestFUSEREADME(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + dir := randTmpDir(t) + d := &fakeDialer{} + _, _, cleanup := newTestClient(t, d, dir, randTmpDir(t)) + + fi, err := os.Stat(dir) + if err != nil { + t.Fatalf("os.Stat: %v", err) + } + if !fi.IsDir() { + t.Fatalf("fuse mount mode: want = dir, got = %v", fi.Mode()) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("os.ReadDir: %v", err) + } + if len(entries) != 1 { + t.Fatalf("dir entries: want = 1, got = %v", len(entries)) + } + e := entries[0] + if want, got := "README", e.Name(); want != got { + t.Fatalf("want = %v, got = %v", want, got) + } + + data, err := os.ReadFile(filepath.Join(dir, "README")) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 { + t.Fatalf("expected README data, got no data (dir = %v)", dir) + } + + cleanup() // close the client + + // verify that the FUSE server is no longer mounted + _, err = os.ReadFile(filepath.Join(dir, "README")) + if err == nil { + t.Fatal("expected os.Readfile to fail, but it succeeded") + } +} + +func tryDialUnix(t *testing.T, addr string) net.Conn { + var ( + conn net.Conn + dialErr error + ) + for i := 0; i < 10; i++ { + conn, dialErr = net.Dial("unix", addr) + if conn != nil { + break + } + time.Sleep(100 * time.Millisecond) + } + if dialErr != nil { + t.Fatalf("net.Dial(): %v", dialErr) + } + return conn +} + +func TestFUSEDialInstance(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + fuseDir := randTmpDir(t) + fuseTempDir := randTmpDir(t) + tcs := []struct { + desc string + wantInstance string + socketPath string + fuseTempDir string + }{ + { + desc: "mysql connections create a Unix socket", + wantInstance: "proj:region:mysql", + socketPath: filepath.Join(fuseDir, "proj:region:mysql"), + fuseTempDir: fuseTempDir, + }, + { + desc: "postgres connections create a directory with a special file", + wantInstance: "proj:region:pg", + socketPath: filepath.Join(fuseDir, "proj:region:pg", ".s.PGSQL.5432"), + fuseTempDir: fuseTempDir, + }, + { + desc: "connecting creates intermediate temp directories", + wantInstance: "proj:region:mysql", + socketPath: filepath.Join(fuseDir, "proj:region:mysql"), + fuseTempDir: filepath.Join(fuseTempDir, "doesntexist"), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + d := &fakeDialer{} + _, _, cleanup := newTestClient(t, d, fuseDir, tc.fuseTempDir) + defer cleanup() + + conn := tryDialUnix(t, tc.socketPath) + defer conn.Close() + + var got []string + for i := 0; i < 10; i++ { + got = d.dialedInstances() + if len(got) == 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + if len(got) != 1 { + t.Fatalf("dialed instances len: want = 1, got = %v", got) + } + if want, inst := tc.wantInstance, got[0]; want != inst { + t.Fatalf("instance: want = %v, got = %v", want, inst) + } + + }) + } +} +func TestFUSEAcceptErrorReturnedFromServe(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + fuseDir := randTmpDir(t) + fuseTempDir := randTmpDir(t) + socketPath := filepath.Join(fuseDir, "proj:region:mysql") + + // Create a new client + d := &fakeDialer{} + c, servErrCh, cleanup := newTestClient(t, d, fuseDir, fuseTempDir) + defer cleanup() + + // Attempt a successful connection to the client + conn := tryDialUnix(t, socketPath) + defer conn.Close() + + // Ensure that the client actually fully connected. + // This solves a race condition in the test that is only present on + // the Ubuntu-Latest platform. + var got []string + for i := 0; i < 10; i++ { + got = d.dialedInstances() + if len(got) == 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + if len(got) != 1 { + t.Fatalf("dialed instances len: want = 1, got = %v", got) + } + + // Explicitly close the dialer. This will close all the unix sockets, forcing + // the unix socket accept goroutine to exit with an error + c.Close() + + // Check that Client.Serve() returned a non-nil error + for i := 0; i < 10; i++ { + select { + case servErr := <-servErrCh: + if servErr == nil { + t.Fatal("got nil, want non-nil error returned by Client.Serve()") + } + return + default: + time.Sleep(100 * time.Millisecond) + continue + } + } + t.Fatal("No error thrown by Client.Serve()") + +} + +func TestFUSEReadDir(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + fuseDir := randTmpDir(t) + _, _, cleanup := newTestClient(t, &fakeDialer{}, fuseDir, randTmpDir(t)) + defer cleanup() + + // Initiate a connection so the FUSE server will list it in the dir entries. + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + entries, err := os.ReadDir(fuseDir) + if err != nil { + t.Fatalf("os.ReadDir(): %v", err) + } + // len should be README plus the proj:reg:mysql socket + if got, want := len(entries), 2; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + if names[0] != "README" || names[1] != "proj:reg:mysql" { + t.Fatalf("want = %v, got = %v", []string{"README", "proj:reg:mysql"}, names) + } +} + +func TestLookupIgnoresContext(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + // create context and cancel it immediately + ctx, cancel := context.WithCancel(context.Background()) + cancel() + d := &fakeDialer{} + c, _, _ := newTestClient(t, d, randTmpDir(t), randTmpDir(t)) + + // invoke Lookup with cancelled context, should ignore context and succeed + _, err := c.Lookup(ctx, "proj:reg:mysql", nil) + if err != fs.OK { + t.Fatalf("proxy.Client.Lookup(): %v", err) + } + // Close the client to close all open sockets. + if err := c.Close(); err != nil { + t.Fatalf("c.Close(): %v", err) + } +} + +func TestFUSEErrors(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + ctx := context.Background() + d := &fakeDialer{} + c, _, _ := newTestClient(t, d, randTmpDir(t), randTmpDir(t)) + + // Simulate FUSE file access by invoking Lookup directly to control + // how the socket cache is populated. + _, err := c.Lookup(ctx, "proj:reg:mysql", nil) + if err != fs.OK { + t.Fatalf("proxy.Client.Lookup(): %v", err) + } + + // Close the client to close all open sockets. + if err := c.Close(); err != nil { + t.Fatalf("c.Close(): %v", err) + } + + // Simulate another FUSE file access to directly populated the socket cache. + _, err = c.Lookup(ctx, "proj:reg:mysql", nil) + if err != fs.OK { + t.Fatalf("proxy.Client.Lookup(): %v", err) + } + + // Verify the dialer was called twice, to prove the previous cache entry was + // removed when the socket was closed. + var attempts int + wantAttempts := 2 + for i := 0; i < 10; i++ { + attempts = d.engineVersionAttempts() + if attempts == wantAttempts { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("engine version attempts: want = %v, got = %v", wantAttempts, attempts) +} + +func TestFUSEWithBadInstanceName(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + fuseDir := randTmpDir(t) + d := &fakeDialer{} + _, _, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) + defer cleanup() + + _, dialErr := net.Dial("unix", filepath.Join(fuseDir, "notvalid")) + if dialErr == nil { + t.Fatalf("net.Dial() should fail") + } + + if got := d.engineVersionAttempts(); got > 0 { + t.Fatalf("engine version calls: want = 0, got = %v", got) + } +} + +func TestFUSECheckConnections(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + fuseDir := randTmpDir(t) + d := &fakeDialer{} + c, _, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) + defer cleanup() + + // first establish a connection to "register" it with the proxy + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + n, err := c.CheckConnections(context.Background()) + if err != nil { + t.Fatalf("c.CheckConnections(): %v", err) + } + if want, got := 1, n; want != got { + t.Fatalf("CheckConnections number of connections: want = %v, got = %v", want, got) + } + + // verify the dialer was invoked twice, once for connect, once for check + // connection + var attempts int + wantAttempts := 2 + for i := 0; i < 10; i++ { + attempts = d.dialAttempts() + if attempts == wantAttempts { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("dial attempts: want = %v, got = %v", wantAttempts, attempts) +} + +func TestFUSEClose(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + fuseDir := randTmpDir(t) + d := &fakeDialer{} + c, _, _ := newTestClient(t, d, fuseDir, randTmpDir(t)) + + // first establish a connection to "register" it with the proxy + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + // Close the proxy which should close all listeners + if err := c.Close(); err != nil { + t.Fatalf("c.Close(): %v", err) + } + + _, err := net.Dial("unix", filepath.Join(fuseDir, "proj:reg:mysql")) + if err == nil { + t.Fatal("net.Dial() should fail") + } +} + +func TestFUSEWithBadDir(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + conf := &proxy.Config{FUSEDir: "/not/a/dir", FUSETempDir: randTmpDir(t)} + _, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, conf, nil) + if err == nil { + t.Fatal("proxy client should fail with bad dir") + } +} diff --git a/internal/proxy/fuse_windows.go b/internal/proxy/fuse_windows.go new file mode 100644 index 000000000..dd1e3b265 --- /dev/null +++ b/internal/proxy/fuse_windows.go @@ -0,0 +1,47 @@ +// 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 + +import ( + "context" + "errors" + "path/filepath" + "strings" +) + +var errFUSENotSupported = errors.New("FUSE is not supported on Windows") + +// SupportsFUSE is false on Windows. +func SupportsFUSE() error { + return errFUSENotSupported +} + +// UnixAddress returns the Unix socket for a given instance in the provided +// directory, by replacing all colons in the instance's name with periods. +func UnixAddress(dir, inst string) string { + inst2 := strings.ReplaceAll(inst, ":", ".") + return filepath.Join(dir, inst2) +} + +type fuseMount struct { + // fuseDir is always an empty string on Windows. + fuseDir string +} + +func configureFUSE(c *Client, conf *Config) (*Client, error) { return nil, errFUSENotSupported } +func (c *Client) fuseMounts() []*socketMount { return nil } +func (c *Client) serveFuse(ctx context.Context, notify func()) error { return errFUSENotSupported } +func (c *Client) unmountFUSE() error { return nil } +func (c *Client) waitForFUSEMounts() {} diff --git a/internal/proxy/internal_test.go b/internal/proxy/internal_test.go new file mode 100644 index 000000000..6523ed472 --- /dev/null +++ b/internal/proxy/internal_test.go @@ -0,0 +1,79 @@ +// 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 + +import ( + "testing" + "unsafe" + + "github.com/google/go-cmp/cmp" +) + +func TestClientUsesSyncAtomicAlignment(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{} //nolint:staticcheck + + if a := unsafe.Offsetof(c.connCount); a%64 != 0 { + t.Errorf("Client.connCount is not 64-bit aligned: want 0, got %v", a) + } +} + +func TestParseImpersonationChain(t *testing.T) { + tcs := []struct { + desc string + in string + wantTarget string + wantChain []string + }{ + { + desc: "when there is only a target", + in: "sv1@developer.gserviceaccount.com", + wantTarget: "sv1@developer.gserviceaccount.com", + }, + { + desc: "when there are delegates", + in: "sv1@developer.gserviceaccount.com,sv2@developer.gserviceaccount.com,sv3@developer.gserviceaccount.com", + wantTarget: "sv1@developer.gserviceaccount.com", + wantChain: []string{ + "sv3@developer.gserviceaccount.com", + "sv2@developer.gserviceaccount.com", + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + gotTarget, gotChain := parseImpersonationChain(tc.in) + if gotTarget != tc.wantTarget { + t.Fatalf("target: want = %v, got = %v", tc.wantTarget, gotTarget) + } + if !equalSlice(tc.wantChain, gotChain) { + t.Fatalf("want chain != got chain: %v", cmp.Diff(tc.wantChain, gotChain)) + } + }) + } +} + +func equalSlice[T comparable](x, y []T) bool { + if len(x) != len(y) { + return false + } + for i := 0; i < len(x); i++ { + if x[i] != y[i] { + return false + } + } + return true +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go new file mode 100644 index 000000000..ff58c552a --- /dev/null +++ b/internal/proxy/proxy.go @@ -0,0 +1,1034 @@ +// 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. + +package proxy + +import ( + "context" + "fmt" + "io" + "net" + "os" + "path" + "regexp" + "strings" + "sync" + "sync/atomic" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/gcloud" + "golang.org/x/oauth2" + "google.golang.org/api/impersonate" + "google.golang.org/api/option" + "google.golang.org/api/sqladmin/v1" +) + +var ( + // Instance connection name is the format :: + // Additionally, we have to support legacy "domain-scoped" projects (e.g. "google.com:PROJECT") + connNameRegex = regexp.MustCompile("([^:]+(:[^:]+)?):([^:]+):([^:]+)") +) + +// connName represents the "instance connection name", in the format +// "project:region:name". Use the "parseConnName" method to initialize this +// struct. +type connName struct { + project string + region string + name string +} + +func (c *connName) String() string { + return fmt.Sprintf("%s:%s:%s", c.project, c.region, c.name) +} + +// parseConnName initializes a new connName struct. +func parseConnName(cn string) (connName, error) { + b := []byte(cn) + m := connNameRegex.FindSubmatch(b) + if m == nil { + return connName{}, fmt.Errorf( + "invalid instance connection name, want = PROJECT:REGION:INSTANCE, got = %v", + cn, + ) + } + + c := connName{ + project: string(m[1]), + region: string(m[3]), + name: string(m[4]), + } + return c, nil +} + +// 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 + // UnixSocket is the directory where a Unix socket will be created, + // connected to the Cloud SQL instance. The full path to the socket will be + // UnixSocket + os.PathSeparator + Name. If set, takes precedence over Addr + // and Port. + UnixSocket string + // UnixSocketPath is the path where a Unix socket will be created, + // connected to the Cloud SQL instance. The full path to the socket will be + // UnixSocketPath. If this is a Postgres database, the proxy will ensure that + // the last path element is `.s.PGSQL.5432`, appending this path element if + // necessary. If set, UnixSocketPath takes precedence over UnixSocket, Addr + // and Port. + UnixSocketPath string + // IAMAuthN enables automatic IAM DB Authentication for the instance. + // MySQL and Postgres only. If it is nil, the value was not specified. + IAMAuthN *bool + + // PrivateIP tells the proxy to attempt to connect to the db instance's + // private IP address instead of the public IP address + PrivateIP *bool + + // PSC tells the proxy to attempt to connect to the db instance's + // private service connect endpoint + PSC *bool +} + +// Config contains all the configuration provided by the caller. +type Config struct { + // Filepath is the path to a configuration file. + Filepath string + + // UserAgent is the user agent to use when connecting to the cloudsql instance + UserAgent string + + // Token is the Bearer token used for authorization. + Token string + + // LoginToken is the Bearer token used for Auto IAM AuthN. Used only in + // conjunction with Token. + LoginToken string + + // CredentialsFile is the path to a service account key. + CredentialsFile string + + // CredentialsJSON is a JSON representation of the service account key. + CredentialsJSON string + + // GcloudAuth set whether to use gcloud's config helper to retrieve a + // token for authentication. + GcloudAuth bool + + // 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 + + // APIEndpointURL is the URL of the Google Cloud SQL Admin API. When left blank, + // the proxy will use the main public api: https://sqladmin.googleapis.com/ + APIEndpointURL string + + // UniverseDomain is the universe domain for the TPC environment. When left + // blank, the proxy will use the Google Default Universe (GDU): googleapis.com + UniverseDomain string + + // UnixSocket is the directory where Unix sockets will be created, + // connected to any Instances. If set, takes precedence over Addr and Port. + UnixSocket string + + // FUSEDir enables a file system in user space at the provided path that + // connects to the requested instance only when a client requests it. + FUSEDir string + + // FUSETempDir sets the temporary directory where the FUSE mount will place + // Unix domain sockets connected to Cloud SQL instances. The temp directory + // is not accessed directly. + FUSETempDir string + + // IAMAuthN enables automatic IAM DB Authentication for all instances. + // MySQL and Postgres only. + IAMAuthN bool + + // MaxConnections are the maximum number of connections the Client may + // establish to the Cloud SQL server side proxy before refusing additional + // connections. A zero-value indicates no limit. + MaxConnections uint64 + + // WaitBeforeClose sets the duration to wait after receiving a shutdown signal + // but before closing the process. Not setting this field means to initiate + // the shutdown process immediately. + WaitBeforeClose time.Duration + + // WaitOnClose sets the duration to wait for connections to close before + // shutting down. Not setting this field means to close immediately + // regardless of any open connections. + WaitOnClose time.Duration + + // PrivateIP enables connections via the database server's private IP address + // for all instances. + PrivateIP bool + + // PSC enables connections via the database server's private service connect + // endpoint for all instances + PSC bool + + // AutoIP supports a legacy behavior where the Proxy will connect to + // the first IP address returned from the SQL ADmin API response. This + // setting should be avoided and used only to support legacy Proxy + // users. + AutoIP bool + + // LazyRefresh configures the Go Connector to retrieve connection info + // lazily and as-needed. Otherwise, no background refresh cycle runs. This + // setting is useful in environments where the CPU may be throttled outside + // of a request context, e.g., Cloud Run. + LazyRefresh bool + + // Instances are configuration for individual instances. Instance + // configuration takes precedence over global configuration. + Instances []InstanceConnConfig + + // QuotaProject is the ID of the Google Cloud project to use to track + // API request quotas. + QuotaProject string + + // ImpersonationChain is a comma separated list of one or more service + // accounts. The first entry in the chain is the impersonation target. Any + // additional service accounts after the target are delegates. The + // roles/iam.serviceAccountTokenCreator must be configured for each account + // that will be impersonated. + ImpersonationChain string + + // StructuredLogs sets all output to use JSON in the LogEntry format. + // See https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + StructuredLogs bool + // Quiet controls whether only error messages are logged. + Quiet bool + + // TelemetryProject enables sending metrics and traces to the specified project. + TelemetryProject string + // TelemetryPrefix sets a prefix for all emitted metrics. + TelemetryPrefix string + // TelemetryTracingSampleRate sets the rate at which traces are + // samples. A higher value means fewer traces. + TelemetryTracingSampleRate int + // ExitZeroOnSigterm exits with 0 exit code when Sigterm received + ExitZeroOnSigterm bool + // DisableTraces disables tracing when TelemetryProject is set. + DisableTraces bool + // DisableMetrics disables metrics when TelemetryProject is set. + DisableMetrics bool + + // Prometheus enables a Prometheus endpoint served at the address and + // port specified by HTTPAddress and HTTPPort. + Prometheus bool + // PrometheusNamespace configures the namespace under which metrics are written. + PrometheusNamespace string + + // HealthCheck enables a health check server. It's address and port are + // specified by HTTPAddress and HTTPPort. + HealthCheck bool + + // HTTPAddress sets the address for the health check and prometheus server. + HTTPAddress string + // HTTPPort sets the port for the health check and prometheus server. + HTTPPort string + // AdminPort configures the port for the localhost-only admin server. + AdminPort string + + // Debug enables a debug handler on localhost. + Debug bool + // QuitQuitQuit enables a handler that will shut the Proxy down upon + // receiving a GET or POST request. + QuitQuitQuit bool + // DebugLogs enables debug level logging. + DebugLogs bool + + // OtherUserAgents is a list of space separate user agents that will be + // appended to the default user agent. + OtherUserAgents string + + // RunConnectionTest determines whether the Proxy should attempt a connection + // to all specified instances to verify the network path is valid. + RunConnectionTest bool + + // SkipFailedInstanceConfig determines whether the Proxy should skip failed + // connections to Cloud SQL instances instead of exiting on startup. + // This only applies to Unix sockets. + SkipFailedInstanceConfig bool +} + +// dialOptions interprets appropriate dial options for a particular instance +// configuration +func dialOptions(c Config, i InstanceConnConfig) []cloudsqlconn.DialOption { + var opts []cloudsqlconn.DialOption + + if i.IAMAuthN != nil { + opts = append(opts, cloudsqlconn.WithDialIAMAuthN(*i.IAMAuthN)) + } + + switch { + // If private IP is enabled at the instance level, or private IP is enabled globally + // add the option. + case i.PrivateIP != nil && *i.PrivateIP || i.PrivateIP == nil && c.PrivateIP: + opts = append(opts, cloudsqlconn.WithPrivateIP()) + // If PSC is enabled at the instance level, or PSC is enabled globally + // add the option. + case i.PSC != nil && *i.PSC || i.PSC == nil && c.PSC: + opts = append(opts, cloudsqlconn.WithPSC()) + case c.AutoIP: + opts = append(opts, cloudsqlconn.WithAutoIP()) + default: + // assume public IP by default + } + if networkType(&c, i) == "unix" { + opts = append(opts, cloudsqlconn.WithMdxClientProtocolType("uds")) + } else { + opts = append(opts, cloudsqlconn.WithMdxClientProtocolType("tcp")) + } + + return opts +} + +func parseImpersonationChain(chain string) (string, []string) { + accts := strings.Split(chain, ",") + target := accts[0] + // Assign delegates if the chain is more than one account. Delegation + // goes from last back towards target, e.g., With sa1,sa2,sa3, sa3 + // delegates to sa2, which impersonates the target sa1. + var delegates []string + if l := len(accts); l > 1 { + for i := l - 1; i > 0; i-- { + delegates = append(delegates, accts[i]) + } + } + return target, delegates +} + +const iamLoginScope = "https://www.googleapis.com/auth/sqlservice.login" + +// iamAuthNEnabled returns true if IAM authentication is enabled globally +// or for any instance in the configuration. +func (c *Config) iamAuthNEnabled() bool { + if c.IAMAuthN { + return true + } + for _, inst := range c.Instances { + if inst.IAMAuthN != nil && *inst.IAMAuthN { + return true + } + } + return false +} + +func credentialsOpt(c Config, l cloudsql.Logger) (cloudsqlconn.Option, error) { + // If service account impersonation is configured, set up an impersonated + // credentials token source. + if c.ImpersonationChain != "" { + var iopts []option.ClientOption + if c.UniverseDomain != "" { + iopts = append(iopts, option.WithUniverseDomain(c.UniverseDomain)) + } + switch { + case c.Token != "": + l.Infof("Impersonating service account with OAuth2 token") + iopts = append(iopts, option.WithTokenSource( + oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}), + )) + case c.CredentialsFile != "": + l.Infof("Impersonating service account with the credentials file at %q", c.CredentialsFile) + iopts = append(iopts, option.WithAuthCredentialsFile(option.ServiceAccount, c.CredentialsFile)) + case c.CredentialsJSON != "": + l.Infof("Impersonating service account with JSON credentials environment variable") + iopts = append(iopts, option.WithAuthCredentialsJSON(option.ServiceAccount, []byte(c.CredentialsJSON))) + case c.GcloudAuth: + l.Infof("Impersonating service account with gcloud user credentials") + ts, err := gcloud.TokenSource() + if err != nil { + return nil, err + } + iopts = append(iopts, option.WithTokenSource(ts)) + default: + l.Infof("Impersonating service account with Application Default Credentials") + } + target, delegates := parseImpersonationChain(c.ImpersonationChain) + ts, err := impersonate.CredentialsTokenSource( + context.Background(), + impersonate.CredentialsConfig{ + TargetPrincipal: target, + Delegates: delegates, + Scopes: []string{sqladmin.SqlserviceAdminScope}, + }, + iopts..., + ) + if err != nil { + return nil, err + } + + if c.iamAuthNEnabled() { + iamLoginTS, err := impersonate.CredentialsTokenSource( + context.Background(), + impersonate.CredentialsConfig{ + TargetPrincipal: target, + Delegates: delegates, + Scopes: []string{iamLoginScope}, + }, + iopts..., + ) + if err != nil { + return nil, err + } + return cloudsqlconn.WithIAMAuthNTokenSources(ts, iamLoginTS), nil + } + return cloudsqlconn.WithTokenSource(ts), nil + } + + // Otherwise, configure credentials as usual. + var opt cloudsqlconn.Option + switch { + case c.Token != "": + l.Infof("Authorizing with OAuth2 token") + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}) + if c.IAMAuthN { + lts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.LoginToken}) + opt = cloudsqlconn.WithIAMAuthNTokenSources(ts, lts) + } else { + opt = cloudsqlconn.WithTokenSource(ts) + } + case c.CredentialsFile != "": + l.Infof("Authorizing with the credentials file at %q", c.CredentialsFile) + opt = cloudsqlconn.WithCredentialsFile(c.CredentialsFile) + case c.CredentialsJSON != "": + l.Infof("Authorizing with JSON credentials environment variable") + opt = cloudsqlconn.WithCredentialsJSON([]byte(c.CredentialsJSON)) + case c.GcloudAuth: + l.Infof("Authorizing with gcloud user credentials") + ts, err := gcloud.TokenSource() + if err != nil { + return nil, err + } + opt = cloudsqlconn.WithTokenSource(ts) + default: + l.Infof("Authorizing with Application Default Credentials") + // Return no-op options to avoid having to handle nil in caller code + opt = cloudsqlconn.WithOptions() + } + return opt, nil +} + +// DialerOptions builds appropriate list of options from the Config +// values for use by cloudsqlconn.NewClient() +func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) { + opts := []cloudsqlconn.Option{ + cloudsqlconn.WithDNSResolver(), + cloudsqlconn.WithUserAgent(c.UserAgent), + } + co, err := credentialsOpt(*c, l) + if err != nil { + return nil, err + } + opts = append(opts, co) + + if c.DebugLogs { + // nolint:staticcheck + opts = append(opts, cloudsqlconn.WithDebugLogger(l)) + } + if c.APIEndpointURL != "" { + opts = append(opts, cloudsqlconn.WithAdminAPIEndpoint(c.APIEndpointURL)) + } + + if c.UniverseDomain != "" { + opts = append(opts, cloudsqlconn.WithUniverseDomain(c.UniverseDomain)) + } + + if c.iamAuthNEnabled() { + opts = append(opts, cloudsqlconn.WithIAMAuthN()) + } + + if c.QuotaProject != "" { + opts = append(opts, cloudsqlconn.WithQuotaProject(c.QuotaProject)) + } + + if c.LazyRefresh { + opts = append(opts, cloudsqlconn.WithLazyRefresh()) + } + + return opts, nil +} + +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 proxies connections from a local client to the remote server side +// proxy for multiple Cloud SQL instances. +type Client struct { + // connCount tracks the number of all open connections from the Client to + // all Cloud SQL instances. + connCount uint64 + + // conf is the configuration used to initialize the Client. + conf *Config + + dialer cloudsql.Dialer + + // mnts is a list of all mounted sockets for this client + mnts []*socketMount + + logger cloudsql.Logger + + connRefuseNotify func() + + fuseMount +} + +// NewClient completes the initial setup required to get the proxy to a "steady" +// state. +func NewClient(ctx context.Context, d cloudsql.Dialer, l cloudsql.Logger, conf *Config, connRefuseNotify func()) (*Client, error) { + // Check if the caller has configured a dialer. + // Otherwise, initialize a new one. + if d == nil { + dialerOpts, err := conf.DialerOptions(l) + if err != nil { + return nil, fmt.Errorf("error initializing dialer: %v", err) + } + d, err = cloudsqlconn.NewDialer(ctx, dialerOpts...) + if err != nil { + return nil, fmt.Errorf("error initializing dialer: %v", err) + } + } + + c := &Client{ + logger: l, + dialer: d, + connRefuseNotify: connRefuseNotify, + conf: conf, + } + + if conf.FUSEDir != "" { + return configureFUSE(c, conf) + } + + for _, inst := range conf.Instances { + // Initiate refresh operation and warm the cache. + go func(name string) { _, _ = d.EngineVersion(ctx, name) }(inst.Name) + } + + var mnts []*socketMount + pc := newPortConfig(conf.Port) + for _, inst := range conf.Instances { + m, err := c.newSocketMount(ctx, conf, pc, inst) + if err != nil { + if conf.SkipFailedInstanceConfig { + l.Errorf("[%v] Unable to mount socket: %v (skipped due to skip-failed-instance-config flag)", inst.Name, err) + continue + } + + for _, m := range mnts { + mErr := m.Close() + if mErr != nil { + l.Errorf("failed to close mount: %v", mErr) + } + } + return nil, fmt.Errorf("[%v] Unable to mount socket: %v", inst.Name, err) + } + + l.Infof("[%s] Listening on %s", inst.Name, m.Addr()) + mnts = append(mnts, m) + } + c.mnts = mnts + return c, nil +} + +// CheckConnections dials each registered instance and reports the number of +// connections checked and any errors that may have occurred. +func (c *Client) CheckConnections(ctx context.Context) (int, error) { + var ( + wg sync.WaitGroup + errCh = make(chan error, len(c.mnts)) + mnts = c.mnts + ) + if c.fuseDir != "" { + mnts = c.fuseMounts() + } + for _, mnt := range mnts { + wg.Add(1) + go func(m *socketMount) { + defer wg.Done() + conn, err := c.dialer.Dial(ctx, m.inst, m.dialOpts...) + if err != nil { + errCh <- err + return + } + cErr := conn.Close() + if cErr != nil { + c.logger.Errorf( + "connection check failed to close connection for %v: %v", + m.inst, cErr, + ) + } + }(mnt) + } + wg.Wait() + + var mErr MultiErr + for i := 0; i < len(mnts); i++ { + select { + case err := <-errCh: + mErr = append(mErr, err) + default: + continue + } + } + mLen := len(mnts) + if len(mErr) > 0 { + return mLen, mErr + } + return mLen, nil +} + +// ConnCount returns the number of open connections and the maximum allowed +// connections. Returns 0 when the maximum allowed connections have not been set. +func (c *Client) ConnCount() (uint64, uint64) { + return atomic.LoadUint64(&c.connCount), c.conf.MaxConnections +} + +// Serve starts proxying connections for all configured instances using the +// associated socket. +func (c *Client) Serve(ctx context.Context, notify func()) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.fuseDir != "" { + return c.serveFuse(ctx, notify) + } + + if c.conf.RunConnectionTest { + c.logger.Infof("Connection test started") + if _, err := c.CheckConnections(ctx); err != nil { + c.logger.Errorf("Connection test failed") + return err + } + c.logger.Infof("Connection test passed") + } + + 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) + } + notify() + return <-exitCh +} + +// MultiErr is a group of errors wrapped into one. +type MultiErr []error + +// Error returns a single string representing one or more errors. +func (m MultiErr) Error() string { + l := len(m) + if l == 1 { + return m[0].Error() + } + var errs []string + for _, e := range m { + errs = append(errs, e.Error()) + } + return strings.Join(errs, ", ") +} + +// Close triggers the proxyClient to shut down. +func (c *Client) Close() error { + mnts := c.mnts + var mErr MultiErr + + // If FUSE is enabled, unmount it and save a reference to any existing + // socket mounts. + if c.fuseDir != "" { + if err := c.unmountFUSE(); err != nil { + mErr = append(mErr, err) + } + mnts = c.fuseMounts() + } + + // Close the dialer to prevent any additional refreshes. + cErr := c.dialer.Close() + if cErr != nil { + mErr = append(mErr, cErr) + } + + // Start a timer for clean shutdown (where all connections are closed). + // While the timer runs, additional connections will be accepted. + timeout := time.After(c.conf.WaitOnClose) + t := time.NewTicker(100 * time.Millisecond) + defer t.Stop() + for { + select { + case <-t.C: + if atomic.LoadUint64(&c.connCount) > 0 { + continue + } + case <-timeout: + } + break + } + // Close all open socket listeners. Time to complete shutdown. + for _, m := range mnts { + err := m.Close() + if err != nil { + mErr = append(mErr, err) + } + } + if c.fuseDir != "" { + c.waitForFUSEMounts() + } + // Verify that all connections are closed. + open := atomic.LoadUint64(&c.connCount) + if c.conf.WaitOnClose > 0 && open > 0 { + openErr := fmt.Errorf( + "%d connection(s) still open after waiting %v", open, c.conf.WaitOnClose) + mErr = append(mErr, openErr) + } + if len(mErr) > 0 { + return mErr + } + return nil +} + +// serveSocketMount persistently listens to the socketMounts listener and proxies connections to a +// given Cloud SQL instance. +func (c *Client) serveSocketMount(_ context.Context, s *socketMount) error { + for { + cConn, err := s.Accept() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + c.logger.Errorf("[%s] Error accepting connection: %v", 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.logger.Infof("[%s] Accepted connection from %s", s.inst, cConn.RemoteAddr()) + + // A client has established a connection to the local socket. Before + // we initiate a connection to the Cloud SQL backend, increment the + // connection counter. If the total number of connections exceeds + // the maximum, refuse to connect and close the client connection. + count := atomic.AddUint64(&c.connCount, 1) + defer atomic.AddUint64(&c.connCount, ^uint64(0)) + + if c.conf.MaxConnections > 0 && count > c.conf.MaxConnections { + c.logger.Infof("max connections (%v) exceeded, refusing new connection", c.conf.MaxConnections) + if c.connRefuseNotify != nil { + go c.connRefuseNotify() + } + _ = cConn.Close() + return + } + + // 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, s.dialOpts...) + if err != nil { + c.logger.Errorf("[%s] failed to connect to instance: %v", 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 + dialOpts []cloudsqlconn.DialOption +} + +func networkType(conf *Config, inst InstanceConnConfig) string { + if (conf.UnixSocket == "" && inst.UnixSocket == "" && inst.UnixSocketPath == "") || + (inst.Addr != "" || inst.Port != 0) { + return "tcp" + } + return "unix" +} + +func (c *Client) newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst InstanceConnConfig) (*socketMount, error) { + var ( + // network is one of "tcp" or "unix" + network string + // address is either a TCP host port, or a Unix socket + address string + err error + ) + // IF + // a global Unix socket directory is NOT set AND + // an instance-level Unix socket is NOT set + // (e.g., I didn't set a Unix socket globally or for this instance) + // OR + // an instance-level TCP address or port IS set + // (e.g., I'm overriding any global settings to use TCP for this + // instance) + // use a TCP listener. + // Otherwise, use a Unix socket. + if networkType(conf, inst) == "tcp" { + network = "tcp" + + a := conf.Addr + if inst.Addr != "" { + a = inst.Addr + } + + var np int + switch { + case inst.Port != 0: + np = inst.Port + case conf.Port != 0: + np = pc.nextPort() + default: + version, err := c.dialer.EngineVersion(ctx, inst.Name) + // Exit if the port is not specified for inactive instance + if err != nil { + c.logger.Errorf("[%v] could not resolve instance version: %v", inst.Name, err) + return nil, err + } + np = pc.nextDBPort(version) + } + + address = net.JoinHostPort(a, fmt.Sprint(np)) + } else { + network = "unix" + + version, err := c.dialer.EngineVersion(ctx, inst.Name) + if err != nil { + c.logger.Errorf("[%v] could not resolve instance version: %v", inst.Name, err) + return nil, err + } + + address, err = newUnixSocketMount(inst, conf.UnixSocket, strings.HasPrefix(version, "POSTGRES")) + if err != nil { + c.logger.Errorf("[%v] could not mount unix socket %q: %v", inst.Name, conf.UnixSocket, err) + return nil, err + } + } + + lc := net.ListenConfig{KeepAlive: 30 * time.Second} + ln, err := lc.Listen(ctx, network, address) + if err != nil { + c.logger.Errorf("[%v] could not listen to address %v: %v", inst.Name, address, err) + return nil, err + } + // Change file permissions to allow access for user, group, and other. + if network == "unix" { + // Best effort. If this call fails, group and other won't have write + // access. + _ = os.Chmod(address, 0777) + } + opts := dialOptions(*conf, inst) + m := &socketMount{inst: inst.Name, dialOpts: opts, listener: ln} + return m, nil +} + +// newUnixSocketMount parses the configuration and returns the path to the unix +// socket, or an error if that path is not valid. +func newUnixSocketMount(inst InstanceConnConfig, unixSocketDir string, postgres bool) (string, error) { + var ( + // the path to the unix socket + address string + // the parent directory of the unix socket + dir string + ) + + if inst.UnixSocketPath != "" { + // When UnixSocketPath is set + address = inst.UnixSocketPath + + // If UnixSocketPath ends .s.PGSQL.5432, remove it for consistency + if postgres && path.Base(address) == ".s.PGSQL.5432" { + address = path.Dir(address) + } + + dir = path.Dir(address) + } else { + // When UnixSocket is set + dir = unixSocketDir + if dir == "" { + dir = inst.UnixSocket + } + address = UnixAddress(dir, inst.Name) + } + + // if base directory does not exist, fail + if _, err := os.Stat(dir); err != nil { + return "", err + } + + // When setting up a listener for Postgres, create address as a + // directory, and use the Postgres-specific socket name + // .s.PGSQL.5432. + if postgres { + // Make the directory only if it hasn't already been created. + if _, err := os.Stat(address); err != nil { + if err = os.Mkdir(address, 0777); err != nil { + return "", err + } + } + address = UnixAddress(address, ".s.PGSQL.5432") + } + + return address, nil +} + +func (s *socketMount) Addr() net.Addr { + return s.listener.Addr() +} + +func (s *socketMount) Accept() (net.Conn, error) { + return s.listener.Accept() +} + +// Close stops the mount from listening for any more connections +func (s *socketMount) Close() error { + return s.listener.Close() +} + +// 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.logger.Errorf(errDesc) + } else { + c.logger.Infof(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, sErr), 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, cErr), true) + return + } + } +} diff --git a/internal/proxy/proxy_other.go b/internal/proxy/proxy_other.go new file mode 100644 index 000000000..b3c7180f3 --- /dev/null +++ b/internal/proxy/proxy_other.go @@ -0,0 +1,210 @@ +// 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. + +//go:build !windows && !openbsd && !freebsd + +package proxy + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +type socketSymlink struct { + socket *socketMount + symlink *symlink +} + +func configureFUSE(c *Client, conf *Config) (*Client, error) { + if _, err := os.Stat(conf.FUSEDir); err != nil { + return nil, err + } + if err := os.MkdirAll(conf.FUSETempDir, 0777); err != nil { + return nil, err + } + c.fuseMount = fuseMount{ + fuseDir: conf.FUSEDir, + fuseTempDir: conf.FUSETempDir, + fuseSockets: map[string]socketSymlink{}, + // Use pointers for the following mutexes so fuseMount may be embedded + // as a value and support zero value lookups on fuseDir. + fuseMu: &sync.Mutex{}, + fuseServerMu: &sync.Mutex{}, + fuseWg: &sync.WaitGroup{}, + } + return c, nil +} + +type fuseMount struct { + // fuseDir specifies the directory where a FUSE server is mounted. The value + // is empty if FUSE is not enabled. The directory holds symlinks to Unix + // domain sockets in the fuseTmpDir. + fuseDir string + fuseTempDir string + // fuseMu protects access to fuseSockets. + fuseMu *sync.Mutex + // fuseSockets is a map of instance connection name to socketMount and + // symlink. + fuseSockets map[string]socketSymlink + fuseServerMu *sync.Mutex + fuseServer *fuse.Server + fuseWg *sync.WaitGroup + fuseExitCh chan error + + // Inode adds support for FUSE operations. + fs.Inode +} + +// Readdir returns a list of all active Unix sockets in addition to the README. +func (c *Client) Readdir(_ context.Context) (fs.DirStream, syscall.Errno) { + entries := []fuse.DirEntry{ + {Name: "README", Mode: 0555 | fuse.S_IFREG}, + } + var active []string + c.fuseMu.Lock() + for k := range c.fuseSockets { + active = append(active, k) + } + c.fuseMu.Unlock() + + for _, a := range active { + entries = append(entries, fuse.DirEntry{ + Name: a, + Mode: 0777 | syscall.S_IFSOCK, + }) + } + return fs.NewListDirStream(entries), fs.OK +} + +// Lookup implements the fs.NodeLookuper interface and returns an index node +// (inode) for a symlink that points to a Unix domain socket. The Unix domain +// socket is connected to the requested Cloud SQL instance. Lookup returns a +// symlink (instead of the socket itself) so that multiple callers all use the +// same Unix socket. +func (c *Client) Lookup(_ context.Context, instance string, _ *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + ctx := context.Background() + if instance == "README" { + return c.NewInode(ctx, &readme{}, fs.StableAttr{}), fs.OK + } + + if _, err := parseConnName(instance); err != nil { + c.logger.Debugf("could not parse instance connection name for %q: %v", instance, err) + return nil, syscall.ENOENT + } + + c.fuseMu.Lock() + defer c.fuseMu.Unlock() + if l, ok := c.fuseSockets[instance]; ok { + c.logger.Debugf("found existing socket for instance %q", instance) + return l.symlink.EmbeddedInode(), fs.OK + } + + c.logger.Debugf("creating new socket for instance %q", instance) + s, err := c.newSocketMount( + ctx, withUnixSocket(*c.conf, c.fuseTempDir), + nil, InstanceConnConfig{Name: instance}, + ) + if err != nil { + c.logger.Errorf("could not create socket for %q: %v", instance, err) + return nil, syscall.ENOENT + } + + c.fuseWg.Add(1) + go func() { + defer c.fuseWg.Done() + sErr := c.serveSocketMount(ctx, s) + if sErr != nil { + c.logger.Debugf("could not serve socket for instance %q: %v", instance, sErr) + c.fuseMu.Lock() + defer c.fuseMu.Unlock() + delete(c.fuseSockets, instance) + 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 c.fuseExitCh <- sErr: + default: + return + } + } + }() + + // Return a symlink that points to the actual Unix socket within the + // temporary directory. For Postgres, return a symlink that points to the + // directory which holds the ".s.PGSQL.5432" Unix socket. + sl := &symlink{path: filepath.Join(c.fuseTempDir, instance)} + c.fuseSockets[instance] = socketSymlink{ + socket: s, + symlink: sl, + } + return c.NewInode(ctx, sl, fs.StableAttr{ + Mode: 0777 | fuse.S_IFLNK}, + ), fs.OK +} + +func withUnixSocket(c Config, tmpDir string) *Config { + c.UnixSocket = tmpDir + return &c +} + +func (c *Client) serveFuse(ctx context.Context, notify func()) error { + srv, err := fs.Mount(c.fuseDir, c, &fs.Options{ + MountOptions: fuse.MountOptions{AllowOther: true}, + }) + if err != nil { + return fmt.Errorf("FUSE mount failed: %q: %v", c.fuseDir, err) + } + c.fuseServerMu.Lock() + c.fuseServer = srv + c.fuseExitCh = make(chan error) + + c.fuseServerMu.Unlock() + notify() + select { + case err = <-c.fuseExitCh: + return err + case <-ctx.Done(): + return ctx.Err() + } +} + +func (c *Client) fuseMounts() []*socketMount { + var mnts []*socketMount + c.fuseMu.Lock() + for _, m := range c.fuseSockets { + mnts = append(mnts, m.socket) + } + c.fuseMu.Unlock() + return mnts +} + +func (c *Client) unmountFUSE() error { + c.fuseServerMu.Lock() + defer c.fuseServerMu.Unlock() + if c.fuseServer == nil { + return nil + } + return c.fuseServer.Unmount() +} + +func (c *Client) waitForFUSEMounts() { c.fuseWg.Wait() } diff --git a/internal/proxy/proxy_other_test.go b/internal/proxy/proxy_other_test.go new file mode 100644 index 000000000..77d7c21bb --- /dev/null +++ b/internal/proxy/proxy_other_test.go @@ -0,0 +1,62 @@ +// 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. + +//go:build !windows + +package proxy_test + +import ( + "context" + "os" + "testing" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" +) + +var ( + pg = "proj:region:pg" + pg2 = "proj:region:pg2" + mysql = "proj:region:mysql" + mysql2 = "proj:region:mysql2" + sqlserver = "proj:region:sqlserver" + sqlserver2 = "proj:region:sqlserver2" +) + +func verifySocketPermissions(t *testing.T, addr string) { + fi, err := os.Stat(addr) + if err != nil { + t.Fatalf("os.Stat(%v): %v", addr, err) + } + if fm := fi.Mode(); fm != 0777|os.ModeSocket { + t.Fatalf("file mode: want = %v, got = %v", 0777|os.ModeSocket, fm) + } +} + +func TestFuseClosesGracefully(t *testing.T) { + c, err := proxy.NewClient( + context.Background(), nil, testLogger, + &proxy.Config{ + FUSEDir: t.TempDir(), + FUSETempDir: t.TempDir(), + Token: "mytoken", + }, + nil) + if err != nil { + t.Fatal(err) + } + if err := c.Close(); err != nil { + t.Fatal(err) + } + +} diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go new file mode 100644 index 000000000..698c6bec2 --- /dev/null +++ b/internal/proxy/proxy_test.go @@ -0,0 +1,833 @@ +// 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" + "errors" + "fmt" + "io" + "net" + "os" + "path" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" +) + +var testLogger = log.NewStdLogger(os.Stdout, os.Stdout) + +type fakeDialer struct { + mu sync.Mutex + dialCount int + engineVersionCount int + instances []string +} + +func (*fakeDialer) Close() error { + return nil +} + +func (f *fakeDialer) dialAttempts() int { + f.mu.Lock() + defer f.mu.Unlock() + return f.dialCount +} + +func (f *fakeDialer) engineVersionAttempts() int { //nolint:unused + f.mu.Lock() + defer f.mu.Unlock() + return f.engineVersionCount +} +func (f *fakeDialer) dialedInstances() []string { //nolint:unused + f.mu.Lock() + defer f.mu.Unlock() + return f.instances +} + +func (f *fakeDialer) Dial(_ context.Context, inst string, _ ...cloudsqlconn.DialOption) (net.Conn, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.dialCount++ + f.instances = append(f.instances, inst) + c1, _ := net.Pipe() + return c1, nil +} + +func (f *fakeDialer) EngineVersion(_ context.Context, inst string) (string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.engineVersionCount++ + 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 + case strings.Contains(inst, "fakeserver"): + return "", fmt.Errorf("non existing server") + default: + return "POSTGRES_14", nil + } +} + +type errorDialer struct { + fakeDialer +} + +func (*errorDialer) Dial(_ context.Context, _ string, _ ...cloudsqlconn.DialOption) (net.Conn, error) { + return nil, errors.New("errorDialer returns error on Dial") +} + +func (*errorDialer) Close() error { + return errors.New("errorDialer returns error on Close") +} + +func createTempDir(t *testing.T) (string, func()) { + testDir, err := os.MkdirTemp("", "*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + return testDir, func() { + if err := os.RemoveAll(testDir); err != nil { + t.Logf("failed to cleanup temp dir: %v", err) + } + } +} + +func TestClientInitialization(t *testing.T) { + ctx := context.Background() + testDir, cleanup := createTempDir(t) + testUnixSocketPath := path.Join(testDir, "db") + testUnixSocketPathPg := path.Join(testDir, "db", ".s.PGSQL.5432") + + defer cleanup() + + tcs := []struct { + desc string + in *proxy.Config + wantTCPAddrs []string + wantUnixAddrs []string + }{ + { + desc: "multiple instances", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 51000, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: mysql}, + {Name: sqlserver}, + }, + }, + wantTCPAddrs: []string{"127.0.0.1:51000", "127.0.0.1:51001", "127.0.0.1:51002"}, + }, + { + desc: "with instance address", + in: &proxy.Config{ + Addr: "1.1.1.1", // bad address, binding shouldn't happen here. + Port: 50003, + Instances: []proxy.InstanceConnConfig{ + {Addr: "0.0.0.0", Name: pg}, + }, + }, + wantTCPAddrs: []string{"0.0.0.0:50003"}, + }, + { + desc: "IPv6 support", + in: &proxy.Config{ + Addr: "::1", + Port: 50004, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + }, + }, + wantTCPAddrs: []string{"[::1]:50004"}, + }, + { + desc: "with instance port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 50005, + Instances: []proxy.InstanceConnConfig{ + {Name: pg, Port: 60000}, + }, + }, + wantTCPAddrs: []string{"127.0.0.1:60000"}, + }, + { + desc: "with global port and instance port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 50006, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: mysql, Port: 60001}, + {Name: sqlserver}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:50006", + "127.0.0.1:60001", + "127.0.0.1:50007", + }, + }, + { + 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}, + }, + }, + wantTCPAddrs: []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", + }, + }, + { + desc: "with a Unix socket", + in: &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: mysql}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testDir, mysql), + }, + }, + { + desc: "with a global TCP host port and an instance Unix socket", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 50008, + Instances: []proxy.InstanceConnConfig{ + {Name: mysql, UnixSocket: testDir}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testDir, mysql), + }, + }, + { + desc: "with a global Unix socket and an instance TCP port", + in: &proxy.Config{ + Addr: "127.0.0.1", + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: pg, Port: 50009}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:50009", + }, + }, + { + desc: "with a Unix socket for Postgres", + in: &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testDir, pg, ".s.PGSQL.5432"), + }, + }, + { + desc: "with a Unix socket path per instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: mysql, UnixSocketPath: testUnixSocketPath}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPath), + }, + }, + { + desc: "with a Unix socket path overriding Unix socket", + in: &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: mysql, UnixSocketPath: testUnixSocketPath}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPath), + }, + }, + { + desc: "with a Unix socket path per pg instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: pg, UnixSocketPath: testUnixSocketPath}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPathPg), + }, + }, + { + desc: "with a Unix socket path per pg instance and explicit pg path suffix", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: pg, UnixSocketPath: testUnixSocketPathPg}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPathPg), + }, + }, + { + desc: "with Unix socket and two instances, one invalid but skipped", + in: &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: "proj:region:fakeserver"}, + }, + SkipFailedInstanceConfig: true, + }, + wantUnixAddrs: []string{ + filepath.Join(testDir, pg, ".s.PGSQL.5432"), + }, + }, + { + desc: "with TCP port for non functional instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:fakeserver", Port: 51010}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:51010", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, tc.in, nil) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + defer func() { + if err := c.Close(); err != nil { + t.Logf("failed to close client: %v", err) + } + }() + for _, addr := range tc.wantTCPAddrs { + 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) + } + } + + for _, addr := range tc.wantUnixAddrs { + verifySocketPermissions(t, addr) + + conn, err := net.Dial("unix", 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) + } + } + }) + } +} + +func TestClientLimitsMaxConnections(t *testing.T) { + d := &fakeDialer{} + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50011, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + MaxConnections: 1, + } + callbackGot := 0 + connRefuseNotify := func() { + d.mu.Lock() + defer d.mu.Unlock() + callbackGot++ + } + c, err := proxy.NewClient(context.Background(), d, testLogger, in, connRefuseNotify) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer c.Close() + go c.Serve(context.Background(), func() {}) + + conn1, err1 := net.Dial("tcp", "127.0.0.1:50011") + if err1 != nil { + t.Fatalf("net.Dial error: %v", err1) + } + defer conn1.Close() + + conn2, err2 := net.Dial("tcp", "127.0.0.1:50011") + if err2 != nil { + t.Fatalf("net.Dial error: %v", err1) + } + defer conn2.Close() + + wantEOF := func(t *testing.T, conns ...net.Conn) { + for _, c := range conns { + // Set a read deadline so any open connections will error on an i/o + // timeout instead of hanging indefinitely. + c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, err := c.Read(make([]byte, 1)) + if err == io.EOF { + return + } + } + t.Fatal("neither connection returned an io.EOF") + } + + // either conn1 or conn2 should be closed + // it doesn't matter which is closed + wantEOF(t, conn1, conn2) + + want := 1 + if got := d.dialAttempts(); got != want { + t.Fatalf("dial attempts did not match expected, want = %v, got = %v", want, got) + } + + if callbackGot == 0 { + t.Fatal("connRefuseNotifyCallback is not called") + } +} + +func tryTCPDial(t *testing.T, addr string) net.Conn { + attempts := 10 + var ( + conn net.Conn + err error + ) + for i := 0; i < attempts; i++ { + conn, err = net.Dial("tcp", addr) + if err != nil { + time.Sleep(100 * time.Millisecond) + continue + } + // Give the socket some time to finish the connection. + time.Sleep(100 * time.Millisecond) + return conn + } + + t.Fatalf("failed to dial in %v attempts: %v", attempts, err) + return nil +} + +func TestClientCloseWaitsForActiveConnections(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50012, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + WaitOnClose: 1 * time.Second, + } + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + go c.Serve(context.Background(), func() {}) + + conn := tryTCPDial(t, "127.0.0.1:50012") + defer conn.Close() + + if err := c.Close(); err == nil { + t.Fatal("c.Close should error, got = nil") + } +} + +func TestClientClosesCleanly(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50013, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst"}, + }, + } + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error want = nil, got = %v", err) + } + go c.Serve(context.Background(), func() {}) + + conn := tryTCPDial(t, "127.0.0.1:50013") + _ = conn.Close() + + if err := c.Close(); err != nil { + t.Fatalf("c.Close() error = %v", err) + } +} + +func TestClosesWithError(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50014, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:reg:inst"}, + }, + } + c, err := proxy.NewClient(context.Background(), &errorDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error want = nil, got = %v", err) + } + go c.Serve(context.Background(), func() {}) + + conn := tryTCPDial(t, "127.0.0.1:50014") + defer conn.Close() + + if err = c.Close(); err == nil { + t.Fatal("c.Close() should error, got nil") + } +} + +func TestMultiErrorFormatting(t *testing.T) { + tcs := []struct { + desc string + in proxy.MultiErr + want string + }{ + { + desc: "with one error", + in: proxy.MultiErr{errors.New("woops")}, + want: "woops", + }, + { + desc: "with many errors", + in: proxy.MultiErr{errors.New("woops"), errors.New("another error")}, + want: "woops, another error", + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + if got := tc.in.Error(); got != tc.want { + t.Errorf("want = %v, got = %v", tc.want, got) + } + }) + } +} + +func TestClientInitializationWorksRepeatedly(t *testing.T) { + // The client creates a Unix socket on initial startup and does not remove + // it on shutdown. This test ensures the existing socket does not cause + // problems for a second invocation. + ctx := context.Background() + testDir, cleanup := createTempDir(t) + defer cleanup() + + in := &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + } + + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + c.Close() + + c, err = proxy.NewClient(ctx, &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + c.Close() +} + +func TestClientNotifiesCallerOnServe(t *testing.T) { + ctx := context.Background() + in := &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + } + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + done := make(chan struct{}) + notify := func() { close(done) } + + go c.Serve(ctx, notify) + + verifyNotification := func(t *testing.T, ch <-chan struct{}) { + for i := 0; i < 10; i++ { + select { + case <-ch: + return + default: + time.Sleep(100 * time.Millisecond) + } + } + t.Fatal("channel should have been closed but was not") + } + verifyNotification(t, done) +} + +func TestClientConnCount(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50015, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + MaxConnections: 10, + } + + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer c.Close() + go c.Serve(context.Background(), func() {}) + + gotOpen, gotMax := c.ConnCount() + if gotOpen != 0 { + t.Fatalf("want 0 open connections, got = %v", gotOpen) + } + if gotMax != 10 { + t.Fatalf("want 10 max connections, got = %v", gotMax) + } + + conn := tryTCPDial(t, "127.0.0.1:50015") + defer conn.Close() + + verifyOpen := func(t *testing.T, want uint64) { + var got uint64 + for i := 0; i < 10; i++ { + got, _ = c.ConnCount() + if got == want { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("open connections, want = %v, got = %v", want, got) + } + verifyOpen(t, 1) +} + +func TestCheckConnections(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50016, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + } + d := &fakeDialer{} + c, err := proxy.NewClient(context.Background(), d, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer c.Close() + go c.Serve(context.Background(), func() {}) + + n, err := c.CheckConnections(context.Background()) + if err != nil { + t.Fatalf("CheckConnections failed: %v", err) + } + if want, got := len(in.Instances), n; want != got { + t.Fatalf("CheckConnections number of connections: want = %v, got = %v", want, got) + } + + if want, got := 1, d.dialAttempts(); want != got { + t.Fatalf("dial attempts: want = %v, got = %v", want, got) + } + + in = &proxy.Config{ + Addr: "127.0.0.1", + Port: 60002, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg1"}, + {Name: "proj:region:pg2"}, + }, + } + ed := &errorDialer{} + c, err = proxy.NewClient(context.Background(), ed, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer c.Close() + go c.Serve(context.Background(), func() {}) + + n, err = c.CheckConnections(context.Background()) + if err == nil { + t.Fatal("CheckConnections should have failed, but did not") + } + if want, got := len(in.Instances), n; want != got { + t.Fatalf("CheckConnections number of connections: want = %v, got = %v", want, got) + } +} + +func TestRunConnectionCheck(t *testing.T) { + in := &proxy.Config{ + Addr: "127.0.0.1", + Port: 50017, + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:pg"}, + }, + RunConnectionTest: true, + } + d := &fakeDialer{} + c, err := proxy.NewClient(context.Background(), d, testLogger, in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer func(c *proxy.Client) { + err := c.Close() + if err != nil { + t.Log(err) + } + }(c) + go func() { + // Serve alone without any connections will still verify that the + // provided instances are reachable. + _ = c.Serve(context.Background(), func() {}) + }() + + verifyDialAttempts := func() error { + var tries int + for { + tries++ + if tries == 10 { + return errors.New("failed to verify dial tries after 10 tries") + } + if got := d.dialAttempts(); got > 0 { + return nil + } + time.Sleep(100 * time.Millisecond) + } + } + + if err := verifyDialAttempts(); err != nil { + t.Fatal(err) + } + +} + +func TestProxyInitializationWithFailedUnixSocket(t *testing.T) { + ctx := context.Background() + testDir, _ := createTempDir(t) + testUnixSocketPath := path.Join(testDir, "db") + + tcs := []struct { + desc string + in *proxy.Config + }{ + { + desc: "with unix socket for non functional instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + { + Name: "proj:region:fakeserver", + UnixSocketPath: testUnixSocketPath, + }, + }, + }, + }, + { + desc: "without TCP port or unix socket for non functional instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:fakeserver"}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, tc.in, nil) + if err == nil { + t.Fatalf("want non nil error, got = %v", err) + } + }) + } +} + +func TestProxyMultiInstances(t *testing.T) { + ctx := context.Background() + testDir, _ := createTempDir(t) + testUnixSocketPath := path.Join(testDir, "db") + + tcs := []struct { + desc string + in *proxy.Config + wantSuccess bool + }{ + { + desc: "with tcp socket and unix for non functional instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + { + Name: "proj:region:fakeserver", + UnixSocketPath: testUnixSocketPath, + }, + {Name: mysql, Port: 3306}, + }, + }, + wantSuccess: false, + }, + { + desc: "with two tcp socket instances and conflicting ports", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: "proj:region:fakeserver", Port: 60003}, + {Name: mysql, Port: 60003}, + }, + }, + wantSuccess: false, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + _, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, tc.in, nil) + if tc.wantSuccess != (err == nil) { + t.Fatalf("want return = %v, got = %v", tc.wantSuccess, err == nil) + } + }) + } +} diff --git a/internal/proxy/proxy_windows_test.go b/internal/proxy/proxy_windows_test.go new file mode 100644 index 000000000..c266d3e52 --- /dev/null +++ b/internal/proxy/proxy_windows_test.go @@ -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. + +package proxy_test + +import ( + "strings" + "testing" +) + +var ( + pg = strings.ReplaceAll("proj:region:pg", ":", ".") + pg2 = strings.ReplaceAll("proj:region:pg2", ":", ".") + mysql = strings.ReplaceAll("proj:region:mysql", ":", ".") + mysql2 = strings.ReplaceAll("proj:region:mysql2", ":", ".") + sqlserver = strings.ReplaceAll("proj:region:sqlserver", ":", ".") + sqlserver2 = strings.ReplaceAll("proj:region:sqlserver2", ":", ".") +) + +func verifySocketPermissions(t *testing.T, addr string) { + // On Linux and Darwin, we check that the socket named by addr exists with + // os.Stat. That operation is not supported on Windows. + // See https://github.com/microsoft/Windows-Containers/issues/97#issuecomment-887713195 +} diff --git a/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go b/internal/proxy/unix.go similarity index 63% rename from cmd/cloud_sql_proxy/cloud_sql_proxy_test.go rename to internal/proxy/unix.go index 4319379bc..7ecd59517 100644 --- a/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go +++ b/internal/proxy/unix.go @@ -12,22 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +//go:build !windows -import ( - "os" - "strings" - "testing" -) +package proxy -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)) +import "path/filepath" - if got := semanticVersion(); got != want { - t.Fatalf("want = %q, got = %q", want, got) - } +// UnixAddress is defined as a function to distinguish between Unix-based +// implementations where the dir and inst are simply joined, and Windows-based +// implementations where the inst must be further altered. +func UnixAddress(dir, inst string) string { + return filepath.Join(dir, inst) } diff --git a/logging/logging.go b/logging/logging.go deleted file mode 100644 index ec6d20f8c..000000000 --- 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/limits/limits_windows.go b/main.go similarity index 50% rename from proxy/limits/limits_windows.go rename to main.go index 9bfab790b..4a73ee881 100644 --- a/proxy/limits/limits_windows.go +++ b/main.go @@ -1,10 +1,10 @@ -// Copyright 2015 Google Inc. All Rights Reserved. +// 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 +// 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, @@ -12,19 +12,14 @@ // 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 +//go:build !windows -import "errors" +package main -// We don't support limit on the number of file handles in windows. -const ExpectedFDs = 0 +import ( + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd" +) -func SetupFDLimits(wantFDs uint64) error { - if wantFDs != 0 { - return errors.New("setting limits on the number of file handles is not supported") - } - - return nil +func main() { + cmd.Execute() } diff --git a/main_windows.go b/main_windows.go new file mode 100644 index 000000000..1dd1b3454 --- /dev/null +++ b/main_windows.go @@ -0,0 +1,127 @@ +// Copyright 2023 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 ( + "context" + "errors" + "os" + "path/filepath" + "time" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "golang.org/x/sys/windows/svc" + "gopkg.in/natefinch/lumberjack.v2" +) + +type windowsService struct{} + +func (m *windowsService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + // start the service + changes <- svc.Status{State: svc.StartPending} + + // set up the log file + exePath, err := os.Executable() + if err != nil { + changes <- svc.Status{State: svc.StopPending} + return true, 101 // service specific exit code=101 + } + + logFolder := filepath.Join(filepath.Dir(exePath), "logs") + os.Mkdir(logFolder, 0644) // ignore all errors + + logFile := &lumberjack.Logger{ + Filename: filepath.Join(logFolder, "cloud-sql-proxy.log"), + MaxSize: 50, // megabytes + MaxBackups: 10, + MaxAge: 30, //days + } + + logger := log.NewStdLogger(logFile, logFile) + logger.Infof("Starting...") + + // start the main command + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + app := cmd.NewCommand(cmd.WithLogger(logger)) + + cmdErrCh := make(chan error, 1) + go func() { + cmdErrCh <- app.ExecuteContext(ctx) + }() + + // now running + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} + + var cmdErr error + +loop: + for { + select { + case err := <-cmdErrCh: + cmdErr = err + break loop + + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + // testing deadlock from https://code.google.com/archive/p/winsvc/issues/4 + time.Sleep(100 * time.Millisecond) + changes <- c.CurrentStatus + + case svc.Stop, svc.Shutdown: + cancel() + + default: + logger.Errorf("unexpected control request #%d", c) + } + } + } + + // start shutting down + logger.Infof("Stopping...") + + changes <- svc.Status{State: svc.StopPending} + + if cmdErr != nil && errors.Is(cmdErr, context.Canceled) { + logger.Errorf("Unexpected error: %v", cmdErr) + return true, 2 + } + + return false, 0 +} + +func main() { + // determine if running as a windows service + inService, err := svc.IsWindowsService() + if err != nil { + os.Exit(99) // failed to determine service status + } + + // running as service? + if inService { + err := svc.Run("cloud-sql-proxy", &windowsService{}) + if err != nil { + os.Exit(100) // failed to execute service + } + return + } + + // run as commandline + cmd.Execute() +} diff --git a/migration-guide.md b/migration-guide.md new file mode 100644 index 000000000..1bc0dfc9f --- /dev/null +++ b/migration-guide.md @@ -0,0 +1,172 @@ +# Migrating from v1 to v2 + +The Cloud SQL Auth Proxy v2 CLI interface maintains a close match to the v1 +interface. Migrating to v2 will require minimal changes. Below are a number +of examples of v1 vs v2 invocations covering the most common uses. See +[Flag Changes](#flag-changes) for details. + +All the examples below use `` as a placeholder for +your instance connection name, e.g., `my-cool-project:us-central1:my-db`. + +## Container Image Name Change + +As part of releasing a v2, we have updated the image name to be more descriptive. +Compare: + +``` +# v1 +gcr.io/cloudsql-docker/gce-proxy +``` + +vs + +``` +# v2 +gcr.io/cloud-sql-connectors/cloud-sql-proxy +``` + +To update to the v2 container, make sure to update the image name. + +## Behavior Differences + +In v1, when a client connected, the Proxy would first try to use a public IP +and then attempt to use a private IP. In v2, the Proxy now defaults to public +IP without trying private IP. If you want to use private IP, you must pass +either the `--private-ip` flag or the query parameter. See the README for details. + +In some cases, the v1 behavior may be preferable. Use the `--auto-ip` flag to +mimic v1 behavior. We generally recommend using deterministic IP address selection, +but recognize in some legacy environments `--auto-ip` may be necessary. + +## Executable Name Change + +Note that the name of the executable has changed, using hyphens rather than underscores: + +```shell +# v1 +./cloud_sql_proxy +``` + +vs + +```shell +# v2 +./cloud-sql-proxy +``` + +## Sample Invocations + +### Listen on TCP socket + +```shell +# v1 +./cloud_sql_proxy -instances==tcp:5432 + +# v2 +# Using automatic database port selection (MySQL 3306, Postgres 5432, SQL Server 1433) +./cloud-sql-proxy +``` + +### Listen on Unix Socket + +```shell +# v1 +./cloud_sql_proxy -dir /cloudsql -instances= + +# v2 +./cloud-sql-proxy --unix-socket /cloudsql +``` + +### Listen on multiple TCP sockets with incrementing ports + +```shell +# v1 +./cloud_sql_proxy -instances==tcp:5000,=tcp:5001 + +# v2 +# starts listener on port 5000, increments for additional listeners +./cloud-sql-proxy --port 5000 +``` + +### Listen on multiple TCP sockets with non-sequential ports + +```shell +# v1 +./cloud_sql_proxy -instances==tcp:6000,=tcp:7000 + +# v2 +./cloud-sql-proxy '?port=6000' '?port=7000' +``` + +### Listen on all interfaces + +```shell +# v1 +./cloud_sql_proxy -instances==tcp:0.0.0.0:6000 + +# v2 +./cloud-sql-proxy --address 0.0.0.0 --port 6000 +``` + +## Environment variable changes + +In v1 it was possible to do this: + +``` shell +export INSTANCES="=tcp:3306,=tcp:5432" + +./cloud_sql_proxy +``` + +In v2, we've significantly expanded the support for environment variables. +All flags can be set with an environment variable including instance connection names. + +For example, in v2 this is possible: + +``` shell +export CSQL_PROXY_INSTANCE_CONNECTION_NAME_0="?port=3306" +export CSQL_PROXY_INSTANCE_CONNECTION_NAME_1="?port=5432" + +export CSQL_PROXY_AUTO_IAM_AUTHN=true + +./cloud-sql-proxy +``` + +See the [help message][] for more details. + +[help message]: https://github.com/GoogleCloudPlatform/cloud-sql-proxy/blob/10bec27e4d44c14fe9e68f25fef6c373324e8bab/cmd/root.go#L240-L264 + +## Flag Changes + +The following table lists in alphabetical order v1 flags and their v2 version. + +- 🗓️: Planned +- ❌: Not supported in V2 +- 🤔: Unplanned, but has open feature request + +| v1 | v2 | Notes | +| --------------------------- | --------------------------- | ------------------------------------------------------------------------------------ | +| check_region | ❌ | | +| credential_file | credentials-file | | +| dir | unix-socket | | +| enable_iam_login | auto-iam-authn | | +| fd_rlimit | 🤔 | [Feature Request](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1258) | +| fuse | fuse | | +| fuse_tmp | fuse-temp-dir | | +| health_check_port | http-port | Use --http-address=0.0.0.0 when using a health check in Kubernetes | +| host | sqladmin-api-endpoint | | +| instances_metadata | 🤔 | [Feature Request](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1259) | +| ip_address_types | private-ip | Defaults to public. To connect to a private IP, you must add the --private-ip flag | +| log_debug_stdout | ❌ | v2 logs to stdout, errors to stderr by default | +| max_connections | max-connections | | +| projects | ❌ | v2 prefers explicit connection configuration to avoid user error | +| quiet | quiet | quiet disables all logging except errors | +| quota_project | quota-project | | +| refresh_config_throttle | ❌ | | +| skip_failed_instance_config | skip-failed-instance-config | | +| structured_logs | structured-logs | | +| term_timeout | max-sigterm-delay | | +| token | token | | +| use_http_health_check | health-check | | +| verbose | ❌ | | +| version | version | | diff --git a/proxy/README.md b/proxy/README.md deleted file mode 100644 index 1d0c8ea95..000000000 --- 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 e26aa572e..000000000 --- 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/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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 7f1f78064..000000000 --- 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 68948b488..000000000 --- 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/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 8ddda86b7..000000000 --- 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/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 8df85147f..000000000 --- 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/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 8dc363162..000000000 --- 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/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 6159527bb..000000000 --- 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/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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_test.go b/proxy/fuse/fuse_test.go deleted file mode 100644 index d9461a634..000000000 --- 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 && !darwin -// +build !windows,!darwin - -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/limits/limits.go b/proxy/limits/limits.go deleted file mode 100644 index 1e636b84c..000000000 --- a/proxy/limits/limits.go +++ /dev/null @@ -1,88 +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. - -// +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/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 a8b222df4..000000000 --- a/proxy/limits/limits_freebsd.go +++ /dev/null @@ -1,81 +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. - -// +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/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 4f9bdbb00..000000000 --- a/proxy/limits/limits_test.go +++ /dev/null @@ -1,135 +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. - -// +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/proxy/client.go b/proxy/proxy/client.go deleted file mode 100644 index 89b04ba14..000000000 --- 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/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/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.Round(0)) - 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.Round(time.Second)) - 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().Round(0)) <= 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 123b5ae77..000000000 --- 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 73cfadd14..000000000 --- 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/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 b20318e0b..000000000 --- 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 a1d580217..000000000 --- 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 574bcd564..000000000 --- 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 2075ebb9c..000000000 --- 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/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 d8d3bc7c8..000000000 --- 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 d614f33db..000000000 --- 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 11c0f3711..000000000 --- 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/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 e238fc7fd..000000000 --- a/tests/alldb_test.go +++ /dev/null @@ -1,73 +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" -) - -// 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) { - // Skipping flaky Github Action test - // https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1228 - t.Skip("Unblocking WIF Builds!") - if testing.Short() { - t.Skip("skipping Health Check integration tests") - } - requireAllVars(t) - ctx := context.Background() - - var args []string - args = append(args, fmt.Sprintf("-instances=%s=tcp:%d,%s=tcp:%d,%s=tcp:%d", *mysqlConnName, mysqlPort, *postgresConnName, postgresPort, *sqlserverConnName, sqlserverPort)) - args = append(args, "-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/common_test.go b/tests/common_test.go index 925817198..ae5333d97 100644 --- a/tests/common_test.go +++ b/tests/common_test.go @@ -1,10 +1,10 @@ -// Copyright 2015 Google Inc. All Rights Reserved. +// 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 +// 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, @@ -12,83 +12,41 @@ // 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. +// 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 +// +// -mysql_conn_name, -db_user, -db_pass package tests import ( "bufio" - "bytes" "context" + "errors" "flag" "fmt" "io" - "io/ioutil" - "log" "os" - "os/exec" - "path" - "runtime" "strings" - "testing" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" ) var ( - binPath = "" + impersonatedUser = flag.String( + "impersonated_user", + os.Getenv("IMPERSONATED_USER"), + "Name of the service account that supports impersonation (impersonator must have roles/iam.serviceAccountTokenCreator)", + ) ) -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. +// ProxyExec represents an execution of the Cloud SQL Auth Proxy. type ProxyExec struct { Out io.ReadCloser - cmd *exec.Cmd + cmd *cmd.Command cancel context.CancelFunc closers []io.Closer done chan bool // closed once the cmd is completed @@ -97,46 +55,51 @@ type ProxyExec struct { // 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), - } + // 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() - 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) + + c := cmd.NewCommand(cmd.WithLogger(log.NewStdLogger(pw, pw))) + c.SetArgs(args) + c.SetOut(pw) + c.SetErr(pw) + + p := &ProxyExec{ + Out: pr, + cmd: c, + cancel: cancel, + closers: []io.Closer{pr, pw}, + done: make(chan bool), } - // when process is complete, mark as finished + // Start the command in the background go func() { defer close(p.done) - p.err = p.cmd.Wait() + defer cancel() + p.err = c.ExecuteContext(ctx) }() - return &p, nil + return p, nil } -// Stop sends the pskill signal to the proxy and returns. -func (p *ProxyExec) Kill() { +// 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() error { +func (p *ProxyExec) Wait(ctx context.Context) error { select { + case <-ctx.Done(): + return ctx.Err() case <-p.done: return p.err } } -// Stop sends the pskill signal to the proxy and returns. +// Done returns true if the proxy has exited. func (p *ProxyExec) Done() bool { select { case <-p.done: @@ -146,7 +109,7 @@ func (p *ProxyExec) Done() bool { return false } -// Close releases any resources assotiated with the instance. +// Close releases any resources associated with the instance. func (p *ProxyExec) Close() { p.cancel() for _, c := range p.closers { @@ -154,40 +117,32 @@ func (p *ProxyExec) 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') +// WaitForServe waits until the proxy ready to serve traffic by waiting for a +// known log message (i.e. "ready for new connections"). 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) (string, error) { + in := bufio.NewReader(p.Out) + for { + select { + case <-ctx.Done(): + // dump all output and return it as an error + all, err := io.ReadAll(in) if err != nil { - errCh <- err - return - } - buf.WriteString(s) - if strings.Contains(s, "Ready for new connections") { - errCh <- nil - return + return "", err } + return "", errors.New(string(all)) + default: } - }() - // 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: + s, err := in.ReadString('\n') if err != nil { - return buf.String(), fmt.Errorf("proxy start failed: %w", err) + return "", err + } + if strings.Contains(s, "Error") || strings.Contains(s, "error") { + return "", errors.New(s) + } + if strings.Contains(s, "ready for new connections") { + return s, nil } } - return buf.String(), nil } diff --git a/tests/connection_test.go b/tests/connection_test.go index cd2d4f3ed..84f7e7bd1 100644 --- a/tests/connection_test.go +++ b/tests/connection_test.go @@ -1,10 +1,10 @@ -// Copyright 2020 Google LLC +// 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 +// 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, @@ -12,28 +12,66 @@ // 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" + "net/http" + "net/http/httputil" + "os" "testing" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" ) -// 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() +const connTestTimeout = time.Minute - 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)) +// 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(), + "https://www.googleapis.com/auth/cloud-platform", + ) + 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) + } + if *ipType != "public" { + return tok, "", func() {} + } + 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) + } +} +func keyfile(t *testing.T) string { + path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") + if path == "" { + t.Fatal("GOOGLE_APPLICATION_CREDENTIALS not set") + } + creds, err := os.ReadFile(path) + if err != nil { + t.Fatalf("io.ReadAll(): %v", err) + } + return string(creds) +} +func proxyConnTestWithReady(t *testing.T, args []string, driver, dsn string, ready func() error) { + ctx, cancel := context.WithTimeout(context.Background(), connTestTimeout) + defer cancel() // Start the proxy p, err := StartProxy(ctx, args...) if err != nil { @@ -44,6 +82,9 @@ func proxyConnTest(t *testing.T, connName, driver, dsn string, port int, dir str if err != nil { t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) } + if err := ready(); err != nil { + t.Fatalf("proxy was not ready: %v", err) + } // Connect to the instance db, err := sql.Open(driver, dsn) @@ -53,72 +94,56 @@ func proxyConnTest(t *testing.T, connName, driver, dsn string, port int, dir str 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() +// 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) { + proxyConnTestWithReady(t, args, driver, dsn, func() error { return nil }) +} - maxConn, totConn := 5, 10 +// testHealthCheck verifies that when a proxy client serves the given instance, +// the readiness endpoint serves http.StatusOK. +func testHealthCheck(t *testing.T, connName string) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), connTestTimeout) + defer cancel() - // Start the proxy - p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), fmt.Sprintf("-max_connections=%d", maxConn)) + args := []string{connName, "--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) + _, err = p.WaitForServe(ctx) if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + t.Fatal(err) } - // 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") + var ( + gErr error + resp *http.Response + ) + for i := 0; i < 10; i++ { + resp, gErr = http.Get("http://localhost:9090/readiness") + if gErr != nil { + time.Sleep(100 * time.Millisecond) + continue + } + if resp.StatusCode != http.StatusOK { + time.Sleep(100 * time.Millisecond) + continue + } + return // The response is OK, the test passes. } - db, err := sql.Open(driver, dsn) - if err != nil { - t.Fatalf("unable to connect to db: %s", err) + if gErr != nil { + t.Fatalf("HTTP GET failed: %v", gErr) } - 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() + respBody, dErr := httputil.DumpResponse(resp, true) + if dErr != nil { + t.Fatalf("failed to dump HTTP response: %v", dErr) } + t.Fatalf("HTTP GET failed: response =\n%v", string(respBody)) } diff --git a/tests/dialer_test.go b/tests/dialer_test.go deleted file mode 100644 index bea05d192..000000000 --- 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/fuse_test.go b/tests/fuse_test.go new file mode 100644 index 000000000..a1b57368f --- /dev/null +++ b/tests/fuse_test.go @@ -0,0 +1,85 @@ +// Copyright 2023 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 !windows && !darwin + +package tests + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" +) + +func TestPostgresFUSEConnect(t *testing.T) { + if v := os.Getenv("IP_TYPE"); v == "private" || v == "psc" { + t.Skipf("skipping test because IP_TYPE is set to %v", v) + } + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + tmpDir, cleanup := createTempDir(t) + defer cleanup() + + host := proxy.UnixAddress(tmpDir, *postgresConnName) + dsn := fmt.Sprintf( + "host=%s user=%s password=%s database=%s sslmode=disable", + host, *postgresUser, *postgresPass, *postgresDB, + ) + testFUSE(t, tmpDir, host, dsn) +} + +func testFUSE(t *testing.T, tmpDir, host string, dsn string) { + tmpDir2, cleanup2 := createTempDir(t) + defer cleanup2() + + waitForFUSE := func() error { + var err error + for i := 0; i < 10; i++ { + _, err = os.Stat(host) + if err == nil { + return nil + } + time.Sleep(500 * time.Millisecond) + } + return fmt.Errorf("failed to find FUSE mounted Unix socket: %v", err) + } + + tcs := []struct { + desc string + dbUser string + args []string + }{ + { + desc: "using default fuse", + args: []string{fmt.Sprintf("--fuse=%s", tmpDir), fmt.Sprintf("--fuse-tmp-dir=%s", tmpDir2)}, + }, + { + desc: "using fuse with auto-iam-authn", + args: []string{fmt.Sprintf("--fuse=%s", tmpDir), "--auto-iam-authn"}, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTestWithReady(t, tc.args, "pgx", dsn, waitForFUSE) + // given the kernel some time to unmount the fuse + time.Sleep(100 * time.Millisecond) + }) + } + +} diff --git a/tests/healthcheck_test.go b/tests/healthcheck_test.go deleted file mode 100644 index 9c263d7e0..000000000 --- a/tests/healthcheck_test.go +++ /dev/null @@ -1,61 +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) { - // Skipping flaky Github Action test - // https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1228 - t.Skip("Unblocking WIF Builds!") - ctx := context.Background() - - // Start a listener on a random port. This test doesn't require a specific - // port otherwise. - args := []string{ - fmt.Sprintf("-instances=%s=tcp:0", connName), "-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 index b9c27b370..8b7f48a31 100644 --- a/tests/mysql_test.go +++ b/tests/mysql_test.go @@ -1,11 +1,11 @@ -// Copyright 2020 Google LLC -// +// 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 -// + +// 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. @@ -17,26 +17,29 @@ package tests import ( "flag" - "io/ioutil" - "log" "os" - "path" - "runtime" "testing" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" 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 + 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.") + mysqlMCPConnName = flag.String("mysql_mcp_conn_name", os.Getenv("MYSQL_MCP_CONNECTION_NAME"), "Cloud SQL MCP MYSQL instance connection name, in the form of 'project:region:instance'.") + mysqlMCPPass = flag.String("mysql_mcp_pass", os.Getenv("MYSQL_MCP_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + ipType = flag.String("ip_type", func() string { + if v := os.Getenv("IP_TYPE"); v != "" { + return v + } + return "public" + }(), "IP type of the instance to connect to, can be public, private or psc") ) -func requireMysqlVars(t *testing.T) { +func requireMySQLVars(t *testing.T) { switch "" { case *mysqlConnName: t.Fatal("'mysql_conn_name' not set") @@ -44,75 +47,224 @@ func requireMysqlVars(t *testing.T) { t.Fatal("'mysql_user' not set") case *mysqlPass: t.Fatal("'mysql_pass' not set") - case *mysqlDb: + case *mysqlDB: t.Fatal("'mysql_db' not set") + case *mysqlMCPConnName: + t.Fatal("'mysql_mcp_conn_name' not set") + case *mysqlMCPPass: + t.Fatal("'mysql_mcp_pass' not set") } } -func TestMysqlTcp(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMysqlVars(t) +func mysqlDSN() string { cfg := mysql.Config{ User: *mysqlUser, Passwd: *mysqlPass, - DBName: *mysqlDb, + DBName: *mysqlDB, AllowNativePasswords: true, + Addr: "127.0.0.1:3306", + Net: "tcp", } - proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort, "") + return cfg.FormatDSN() } -func TestMysqlSocket(t *testing.T) { +// AddIPTypeFlag appends the correct flag based on the ipType variable. +func AddIPTypeFlag(args []string) []string { + switch *ipType { + case "private": + return append(args, "--private-ip") + case "psc": + return append(args, "--psc") + default: + return args + } +} + +func TestMySQLTCP(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) + requireMySQLVars(t) + // Prepare the initial arguments + args := []string{*mysqlConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "mysql", mysqlDSN()) +} - dir, err := ioutil.TempDir("", "csql-proxy-tests") - if err != nil { - log.Fatalf("unable to create tmp dir: %s", err) +func TestMySQLUnix(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") } - defer os.RemoveAll(dir) + requireMySQLVars(t) + tmpDir, cleanup := createTempDir(t) + defer cleanup() cfg := mysql.Config{ User: *mysqlUser, Passwd: *mysqlPass, - Net: "unix", - Addr: path.Join(dir, *mysqlConnName), - DBName: *mysqlDb, + DBName: *mysqlDB, AllowNativePasswords: true, + // re-use utility function to determine the Unix address in a + // Windows-friendly way. + Addr: proxy.UnixAddress(tmpDir, *mysqlConnName), + Net: "unix", } - proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), 0, dir) + // Prepare the initial arguments + args := []string{"--unix-socket", tmpDir, *mysqlConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "mysql", cfg.FormatDSN()) } -func TestMysqlConnLimit(t *testing.T) { +func TestMySQLMCPUnix(t *testing.T) { if testing.Short() { t.Skip("skipping MySQL integration tests") } - requireMysqlVars(t) + requireMySQLVars(t) + tmpDir, cleanup := createTempDir(t) + defer cleanup() + cfg := mysql.Config{ User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDb, + Passwd: *mysqlMCPPass, + DBName: *mysqlDB, AllowNativePasswords: true, + // re-use utility function to determine the Unix address in a + // Windows-friendly way. + Addr: proxy.UnixAddress(tmpDir, *mysqlMCPConnName), + Net: "unix", } - proxyConnLimitTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort) + // Prepare the initial arguments + args := []string{"--unix-socket", tmpDir, *mysqlMCPConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "mysql", cfg.FormatDSN()) } -// 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) { +func TestMySQLImpersonation(t *testing.T) { if testing.Short() { t.Skip("skipping MySQL integration tests") } - switch "" { - case *mysqlConnName: - t.Fatal("'mysql_conn_name' not set") + requireMySQLVars(t) + + // Prepare the initial arguments + args := []string{ + "--impersonate-service-account", *impersonatedUser, + *mysqlConnName, + } + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "mysql", mysqlDSN()) +} + +func TestMySQLAuthentication(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMySQLVars(t) + + var creds string + if *ipType == "public" { + creds = keyfile(t) + } + tok, path, cleanup := removeAuthEnvVar(t) + defer cleanup() + + tcs := []struct { + desc string + args []string + }{ + { + desc: "with token", + args: []string{"--token", tok.AccessToken, *mysqlConnName}, + }, + { + desc: "with token and impersonation", + args: []string{ + "--token", tok.AccessToken, + "--impersonate-service-account", *impersonatedUser, + *mysqlConnName}, + }, + } + if *ipType == "public" { + additionaTcs := []struct { + desc string + args []string + }{ + { + desc: "with credentials file", + args: []string{"--credentials-file", path, *mysqlConnName}, + }, + { + desc: "with credentials file and impersonation", + args: []string{ + "--credentials-file", path, + "--impersonate-service-account", *impersonatedUser, + *mysqlConnName, + }, + }, + { + desc: "with credentials JSON", + args: []string{"--json-credentials", string(creds), *mysqlConnName}, + }, + { + desc: "with credentials JSON and impersonation", + args: []string{ + "--json-credentials", string(creds), + "--impersonate-service-account", *impersonatedUser, + *mysqlConnName, + }, + }, + } + tcs = append(tcs, additionaTcs...) + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "mysql", mysqlDSN()) + }) + } +} + +func TestMySQLGcloudAuth(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") } + if v := os.Getenv("IP_TYPE"); v == "private" || v == "psc" { + t.Skipf("skipping test because IP_TYPE is set to %v", v) + } + requireMySQLVars(t) - singleInstanceDial(t, *mysqlConnName) + tcs := []struct { + desc string + args []string + }{ + { + desc: "gcloud user authentication", + args: []string{"--gcloud-auth", *mysqlConnName}, + }, + { + desc: "gcloud user authentication with impersonation", + args: []string{ + "--gcloud-auth", + "--impersonate-service-account", *impersonatedUser, + *mysqlConnName}, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "mysql", mysqlDSN()) + }) + } +} + +func TestMySQLHealthCheck(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + testHealthCheck(t, *mysqlConnName) } diff --git a/tests/other_test.go b/tests/other_test.go new file mode 100644 index 000000000..de9e6021f --- /dev/null +++ b/tests/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/tests/postgres_test.go b/tests/postgres_test.go index 9f2465c61..fe6400d02 100644 --- a/tests/postgres_test.go +++ b/tests/postgres_test.go @@ -1,11 +1,11 @@ -// Copyright 2020 Google LLC -// +// 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 -// + +// 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. @@ -16,31 +16,27 @@ package tests import ( - "context" - "database/sql" "flag" "fmt" - "io/ioutil" - "log" "os" - "path" - "runtime" + "strings" "testing" - "time" - _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres" - _ "github.com/lib/pq" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" + _ "github.com/jackc/pgx/v5/stdlib" ) 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 + 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.") + postgresMCPConnName = flag.String("postgres_mcp_conn_name", os.Getenv("POSTGRES_MCP_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name for a managed instance, in the form of 'project:region:instance'.") + postgresMCPPass = flag.String("postgres_mcp_pass", os.Getenv("POSTGRES_MCP_PASS"), "Password for the managed instance database user; be careful when entering a password on the command line (it may go into your terminal's history).") + postgresCustomerCASConnName = flag.String("postgres_customer_cas_conn_name", os.Getenv("POSTGRES_CUSTOMER_CAS_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name for a customer CAS enabled instance, in the form of 'project:region:instance'.") + postgresCustomerCASPass = flag.String("postgres_customer_cas_pass", os.Getenv("POSTGRES_CUSTOMER_CAS_PASS"), "Password for the customer CAS instance database user; be careful when entering a password on the command line (it may go into your terminal's history).") + postgresCustomerCASDomain = flag.String("postgres_customer_cas_domain", os.Getenv("POSTGRES_CUSTOMER_CAS_DOMAIN_NAME"), "Valid DNS domain name for the customer CAS instance.") ) func requirePostgresVars(t *testing.T) { @@ -51,115 +47,299 @@ func requirePostgresVars(t *testing.T) { t.Fatal("'postgres_user' not set") case *postgresPass: t.Fatal("'postgres_pass' not set") - case *postgresDb: + case *postgresDB: t.Fatal("'postgres_db' not set") } } -func TestPostgresTcp(t *testing.T) { +func postgresDSN() string { + return fmt.Sprintf("host=localhost user=%s password=%s database=%s sslmode=disable", + *postgresUser, *postgresPass, *postgresDB) +} + +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, "") + // Prepare the initial arguments + args := []string{*postgresConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "pgx", postgresDSN()) } -func TestPostgresSocket(t *testing.T) { +func TestPostgresUnix(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) + tmpDir, cleanup := createTempDir(t) + defer cleanup() - 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("host=%s user=%s password=%s database=%s sslmode=disable", + // re-use utility function to determine the Unix address in a + // Windows-friendly way. + proxy.UnixAddress(tmpDir, *postgresConnName), + *postgresUser, *postgresPass, *postgresDB) - 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) + // Prepare the initial arguments + args := []string{"--unix-socket", tmpDir, *postgresConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "pgx", dsn) } -func TestPostgresConnLimit(t *testing.T) { +func TestPostgresMCPUnix(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } requirePostgresVars(t) + tmpDir, cleanup := createTempDir(t) + defer cleanup() + + dsn := fmt.Sprintf("host=%s user=%s password=%s database=%s sslmode=disable", + // re-use utility function to determine the Unix address in a + // Windows-friendly way. + proxy.UnixAddress(tmpDir, *postgresMCPConnName), + *postgresUser, *postgresMCPPass, *postgresDB) - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) - proxyConnLimitTest(t, *postgresConnName, "postgres", dsn, postgresPort) + // Prepare the initial arguments + args := []string{"--unix-socket", tmpDir, *postgresMCPConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "pgx", dsn) } -func TestPostgresIAMDBAuthn(t *testing.T) { +func createTempDir(t *testing.T) (string, func()) { + testDir, err := os.MkdirTemp("", "*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + return testDir, func() { + if err := os.RemoveAll(testDir); err != nil { + t.Logf("failed to cleanup temp dir: %v", err) + } + } +} + +func TestPostgresImpersonation(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } requirePostgresVars(t) - if *postgresIAMUser == "" { - t.Fatal("'postgres_user_iam' not set") + // Prepare the initial arguments + args := []string{ + "--impersonate-service-account", *impersonatedUser, + *postgresConnName, } + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "pgx", postgresDSN()) +} - ctx := context.Background() +func TestPostgresAuthentication(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) - // 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) + var creds string + if *ipType == "public" { + creds = keyfile(t) } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + tok, path, cleanup := removeAuthEnvVar(t) + defer cleanup() + + tcs := []struct { + desc string + args []string + }{ + { + desc: "with token", + args: []string{"--token", tok.AccessToken, *postgresConnName}, + }, + { + desc: "with token and impersonation", + args: []string{ + "--token", tok.AccessToken, + "--impersonate-service-account", *impersonatedUser, + *postgresConnName}, + }, } + if *ipType == "public" { + additionalTcs := []struct { + desc string + args []string + }{ + { + desc: "with credentials file", + args: []string{"--credentials-file", path, *postgresConnName}, + }, + { + desc: "with credentials file and impersonation", + args: []string{ + "--credentials-file", path, + "--impersonate-service-account", *impersonatedUser, + *postgresConnName, + }, + }, + { + desc: "with credentials JSON", + args: []string{"--json-credentials", string(creds), *postgresConnName}, + }, + { + desc: "with credentials JSON and impersonation", + args: []string{ + "--json-credentials", string(creds), + "--impersonate-service-account", *impersonatedUser, + *postgresConnName, + }, + }, + } + tcs = append(tcs, additionalTcs...) + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "pgx", postgresDSN()) + }) + } +} - 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) +func TestPostgresGcloudAuth(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") } - defer db.Close() - _, err = db.Exec("SELECT 1;") - if err != nil { + if v := os.Getenv("IP_TYPE"); v == "private" || v == "psc" { + t.Skipf("skipping test because IP_TYPE is set to %v", v) + } + requirePostgresVars(t) - t.Fatalf("unable to exec on db: %s", err) + tcs := []struct { + desc string + args []string + }{ + { + desc: "gcloud user authentication", + args: []string{"--gcloud-auth", *postgresConnName}, + }, + { + desc: "gcloud user authentication with impersonation", + args: []string{ + "--gcloud-auth", + "--impersonate-service-account", *impersonatedUser, + *postgresConnName}, + }, } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "pgx", postgresDSN()) + }) + } + } -func TestPostgresHook(t *testing.T) { +func TestPostgresIAMDBAuthn(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() + requirePostgresVars(t) + if *postgresIAMUser == "" { + t.Fatal("'postgres_user_iam' not set") + } - 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) + defaultDSN := fmt.Sprintf("host=localhost user=%s database=%s sslmode=disable", + *postgresIAMUser, *postgresDB) + impersonatedIAMUser := strings.ReplaceAll(*impersonatedUser, ".gserviceaccount.com", "") + + tcs := []struct { + desc string + dsn string + args []string + }{ + { + desc: "using default flag", + args: []string{"--auto-iam-authn", *postgresConnName}, + dsn: defaultDSN, + }, + { + desc: "using query param", + args: []string{fmt.Sprintf("%s?auto-iam-authn=true", *postgresConnName)}, + dsn: defaultDSN, + }, + { + desc: "using impersonation", + args: []string{ + "--auto-iam-authn", + "--impersonate-service-account", *impersonatedUser, + *postgresConnName}, + dsn: fmt.Sprintf("host=localhost user=%s database=%s sslmode=disable", + impersonatedIAMUser, *postgresDB), + }, + { + desc: "using impersonation with query param", + args: []string{ + "--impersonate-service-account", *impersonatedUser, + fmt.Sprintf("%s?auto-iam-authn=true", *postgresConnName)}, + dsn: fmt.Sprintf("host=localhost user=%s password=password database=%s sslmode=disable", + impersonatedIAMUser, *postgresDB), + }, } - defer db.Close() - var now time.Time - err = db.QueryRowContext(ctx, "SELECT NOW()").Scan(&now) - if err != nil { - t.Fatalf("query failed: %s", err) + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "pgx", tc.dsn) + }) } } -// 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) { +func TestPostgresCustomerCAS(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } - switch "" { - case *postgresConnName: - t.Fatal("'postgres_conn_name' not set") + requirePostgresVars(t) + if *postgresCustomerCASConnName == "" { + t.Fatal("'postgres_customer_cas_conn_name' not set") + } + if *postgresCustomerCASPass == "" { + t.Fatal("'postgres_customer_cas_pass' not set") } + if *postgresCustomerCASDomain == "" { + t.Fatal("'postgres_customer_cas_domain' not set") + } + + defaultDSN := fmt.Sprintf("host=localhost user=%s password=%s database=%s sslmode=disable", + *postgresUser, *postgresCustomerCASPass, *postgresDB) - singleInstanceDial(t, *postgresConnName) + tcs := []struct { + desc string + dsn string + args []string + }{ + { + desc: "using customer CAS default", + args: []string{*postgresCustomerCASConnName}, + dsn: defaultDSN, + }, + { + desc: "using valid domain name", + args: []string{*postgresCustomerCASDomain}, + dsn: defaultDSN, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "pgx", tc.dsn) + }) + } +} + +func TestPostgresHealthCheck(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + testHealthCheck(t, *postgresConnName) } diff --git a/tests/sqlserver_test.go b/tests/sqlserver_test.go index 7910f32de..921dff6b1 100644 --- a/tests/sqlserver_test.go +++ b/tests/sqlserver_test.go @@ -1,11 +1,11 @@ -// Copyright 2020 Google LLC -// +// 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 -// + +// 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. @@ -21,19 +21,17 @@ import ( "os" "testing" - _ "github.com/denisenkom/go-mssqldb" + _ "github.com/microsoft/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 + sqlserverDB = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") ) -func requireSqlserverVars(t *testing.T) { +func requireSQLServerVars(t *testing.T) { switch "" { case *sqlserverConnName: t.Fatal("'sqlserver_conn_name' not set") @@ -41,41 +39,148 @@ func requireSqlserverVars(t *testing.T) { t.Fatal("'sqlserver_user' not set") case *sqlserverPass: t.Fatal("'sqlserver_pass' not set") - case *sqlserverDb: + case *sqlserverDB: t.Fatal("'sqlserver_db' not set") } } -func TestSqlServerTcp(t *testing.T) { +func sqlserverDSN() string { + return fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", + *sqlserverUser, *sqlserverPass, *sqlserverDB) +} + +func TestSQLServerTCP(t *testing.T) { if testing.Short() { t.Skip("skipping SQL Server integration tests") } - requireSqlserverVars(t) + requireSQLServerVars(t) + // Prepare the initial arguments + args := []string{*sqlserverConnName} + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "sqlserver", sqlserverDSN()) +} - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) - proxyConnTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort, "") +func TestSQLServerImpersonation(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSQLServerVars(t) + // Prepare the initial arguments + args := []string{ + "--impersonate-service-account", *impersonatedUser, + *sqlserverConnName, + } + // Add the IP type flag using the helper + args = AddIPTypeFlag(args) + // Run the test + proxyConnTest(t, args, "sqlserver", sqlserverDSN()) } -func TestSqlserverConnLimit(t *testing.T) { +func TestSQLServerAuthentication(t *testing.T) { if testing.Short() { t.Skip("skipping SQL Server integration tests") } - requireSqlserverVars(t) + requireSQLServerVars(t) + + var creds string + if *ipType == "public" { + creds = keyfile(t) + } + tok, path, cleanup := removeAuthEnvVar(t) + defer cleanup() - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) - proxyConnLimitTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort) + tcs := []struct { + desc string + args []string + }{ + { + desc: "with token", + args: []string{"--token", tok.AccessToken, *sqlserverConnName}, + }, + { + desc: "with token and impersonation", + args: []string{ + "--token", tok.AccessToken, + "--impersonate-service-account", *impersonatedUser, + *sqlserverConnName}, + }, + } + if *ipType == "public" { + additionaTcs := []struct { + desc string + args []string + }{ + { + desc: "with credentials file", + args: []string{"--credentials-file", path, *sqlserverConnName}, + }, + { + desc: "with credentials file and impersonation", + args: []string{ + "--credentials-file", path, + "--impersonate-service-account", *impersonatedUser, + *sqlserverConnName, + }, + }, + { + desc: "with credentials JSON", + args: []string{"--json-credentials", string(creds), *sqlserverConnName}, + }, + { + desc: "with credentials JSON and impersonation", + args: []string{ + "--json-credentials", string(creds), + "--impersonate-service-account", *impersonatedUser, + *sqlserverConnName, + }, + }, + } + tcs = append(tcs, additionaTcs...) + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "sqlserver", sqlserverDSN()) + }) + } } -// 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) { +func TestSQLServerGcloudAuth(t *testing.T) { if testing.Short() { t.Skip("skipping SQL Server integration tests") } - switch "" { - case *sqlserverConnName: - t.Fatal("'sqlserver_conn_name' not set") + if v := os.Getenv("IP_TYPE"); v == "private" || v == "psc" { + t.Skipf("skipping test because IP_TYPE is set to %v", v) } + requireSQLServerVars(t) - singleInstanceDial(t, *sqlserverConnName) + tcs := []struct { + desc string + args []string + }{ + { + desc: "gcloud user authentication", + args: []string{"--gcloud-auth", *sqlserverConnName}, + }, + { + desc: "gcloud user authentication with impersonation", + args: []string{ + "--gcloud-auth", + "--impersonate-service-account", *impersonatedUser, + *sqlserverConnName}, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + proxyConnTest(t, AddIPTypeFlag(tc.args), "sqlserver", sqlserverDSN()) + }) + } +} + +func TestSQLServerHealthCheck(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + testHealthCheck(t, *sqlserverConnName) } diff --git a/windows-service-guide.md b/windows-service-guide.md new file mode 100644 index 000000000..454016a13 --- /dev/null +++ b/windows-service-guide.md @@ -0,0 +1,85 @@ +# Cloud SQL Auth Proxy Windows Service Guide + +This document covers running the *Cloud SQL Auth Proxy* as service +on the Windows operating system. + +It was originally built and tested using Go 1.20.2 on Windows Server 2019. + +## Install the Windows Service + +Prerequisites: A built binary for Windows of the Cloud SQL Auth Proxy is required. Either build it from source or [download a release](https://github.com/GoogleCloudPlatform/cloud-sql-proxy/releases) of a Windows pre-built version, e.g. `cloud-sql-proxy.x64.exe`. + +First, install the binary by: + +1. Create a new empty folder, e.g. `C:\Program Files\cloud-sql-proxy` +2. Copy the binary and helper batch files +3. Modify the batch files as needed: + - `SERVICE` is the Windows internal service name (as shown in the Task Manager) + - `DISPLAYNAME` is the service name (as shown in the Windows Administration Console (MMC)) + - `CREDENTIALSFILE` is the *full* path to the credentials file, where `%~dp0` points to the full path of the script file folder. + - `CONNECTIONNAME` is the Google SQL connection name in the format of `project-id:region:db-instance` + - Please note that the `--credentials-file \"%CREDENTIALSFILE%\"` argument is optional and is not needed if the local machine runs within the Google Cloud Compute Engine and "defaults" to the VM instance service account. +4. Grant *read & execute* access to the `Network Service` user +5. Create a `logs` sub-folder, e.g. `C:\Program Files\cloud-sql-proxy\logs` +6. Grant *modify* access to the `Network Service` user +7. Run the `windows_install_service.bat` batch file within an *elevated* command line prompt (read: *Run as Administrator*). + +After that, perform the setup: + +1. Copy the JSON credentials file, if required +2. Modify the `windows_install_service.bat` file to your needs +3. Run the `windows_install_service.bat` file from the commandline + +Please see the FAQ below for common error messages. + +## Uninstall the Windows Service + +To uninstall the Windows Service, perform the following steps: + +1. Modify the `windows_remove_service.bat` file to your needs +2. Run the `windows_remove_service.bat` file from the commandline + +## FAQ + +### Error Message: *Access is denied* + +The error message `Access is denied.` (or `System error 5 has occurred.`) occurs when +trying to start the installed service but the service account does not have access +to the service's file directory. + +Usually this is the *Network Service* built-in user. + +Please note that write access is also required for creating and managing the log files, e.g.: + +- `cloud-sql-proxy.log` +- `cloud-sql-proxy-2016-11-04T18-30-00.000.log` + +### Error Message: *The specified service has been marked for deletion.* + +The error message `The specified service has been marked for deletion.` occurs when +reinstalling the service and the previous deletion request could not be completed +(e.g. because the service was still running or opened in the service manager). + +In this case, the local machine needs to be restarted. + +### Why not running as the *System* user? + +Since the Cloud Proxy does not require and file system access, besides the log files, +extensive operating system access is not required. + +The *Network Service* accounts allow binding ports while not granting +access to file system resources. + +### Why not using *Automatic (Delayed Start)* startup type? + +The service is installed in the *Automatic* startup type, by default. + +The alternative *Automatic (Delayed Start)* startup type was introduced +by Microsoft for services that are not required for operating system operations +like Windows Update and similar services. + +However, if the primary purpose of the local machine is to provide services +which require access to the cloud database, then the start of the service +should not be delayed. + +Delayed services might be started even minutes after operating system startup. diff --git a/windows_install_service.bat b/windows_install_service.bat new file mode 100644 index 000000000..2c4be1506 --- /dev/null +++ b/windows_install_service.bat @@ -0,0 +1,14 @@ +@echo off + +setlocal + +set SERVICE=cloud-sql-proxy +set DISPLAYNAME=Google Cloud SQL Auth Proxy +set CREDENTIALSFILE=%~dp0key.json +set CONNECTIONNAME=project-id:region:db-instance + +sc.exe create "%SERVICE%" binPath= "\"%~dp0cloud-sql-proxy.exe\" --credentials-file \"%CREDENTIALSFILE%\" %CONNECTIONNAME%" obj= "NT AUTHORITY\Network Service" start= auto displayName= "%DISPLAYNAME%" +sc.exe failure "%SERVICE%" reset= 0 actions= restart/0/restart/0/restart/0 +net start "%SERVICE%" + +endlocal diff --git a/windows_remove_service.bat b/windows_remove_service.bat new file mode 100644 index 000000000..387436b47 --- /dev/null +++ b/windows_remove_service.bat @@ -0,0 +1,10 @@ +@echo off + +setlocal + +set SERVICE=cloud-sql-proxy + +net stop "%SERVICE%" +sc.exe delete "%SERVICE%" + +endlocal