Skip to content

Commit 90ba6c6

Browse files
authored
Merge pull request GoogleCloudPlatform#1 from GoogleCloudPlatform/fork-cloud-sql-proxy
This is an import from the cloudsql-proxy v2 branch at: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/dd7198d940518471166d7f91efa24ae2e50061bc This PR also removes all the v1 paths as well as the: - build - github - kokoro - examples directories.
2 parents 202fc46 + ac46224 commit 90ba6c6

File tree

16 files changed

+2486
-0
lines changed

16 files changed

+2486
-0
lines changed

.envrc.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export GOOGLE_CLOUD_PROJECT="project-name"
2+
3+
export MYSQL_CONNECTION_NAME="project:region:instance"
4+
export MYSQL_USER="mysql-user"
5+
export MYSQL_PASS="mysql-password"
6+
export MYSQL_DB="mysql-db-name"
7+
8+
export POSTGRES_CONNECTION_NAME="project:region:instance"
9+
export POSTGRES_USER="postgres-user"
10+
export POSTGRES_PASS="postgres-password"
11+
export POSTGRES_DB="postgres-db-name"
12+
export POSTGRES_USER_IAM="some-user-with-db-access@example.com"
13+
14+
export SQLSERVER_CONNECTION_NAME="project:region:instance"
15+
export SQLSERVER_USER="sqlserver-user"
16+
export SQLSERVER_PASS="sqlserver-password"
17+
export SQLSERVER_DB="sqlserver-db-name"
18+
19+
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# direnv
2+
.envrc
3+
4+
# IDEs
5+
.idea/
6+
.vscode/
7+
8+
# Compiled binary
9+
/cmd/cloud_sql_proxy/cloud_sql_proxy
10+
/cloud_sql_proxy
11+
# v2 binary
12+
/cloudsql-proxy
13+
14+
/key.json

cloudsql/cloudsql.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cloudsql
16+
17+
import (
18+
"context"
19+
"io"
20+
"net"
21+
22+
"cloud.google.com/go/cloudsqlconn"
23+
)
24+
25+
// Dialer dials a Cloud SQL instance and returns its database engine version.
26+
type Dialer interface {
27+
// Dial returns a connection to the specified instance.
28+
Dial(ctx context.Context, inst string, opts ...cloudsqlconn.DialOption) (net.Conn, error)
29+
// EngineVersion retrieves the provided instance's database version (e.g.,
30+
// POSTGRES_14)
31+
EngineVersion(ctx context.Context, inst string) (string, error)
32+
33+
io.Closer
34+
}

cmd/errors.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2021 Google LLC
2+
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"errors"
19+
)
20+
21+
var (
22+
errSigInt = &exitError{
23+
Err: errors.New("SIGINT signal received"),
24+
Code: 130,
25+
}
26+
27+
errSigTerm = &exitError{
28+
Err: errors.New("SIGTERM signal received"),
29+
Code: 137,
30+
}
31+
)
32+
33+
func newBadCommandError(msg string) error {
34+
return &exitError{
35+
Err: errors.New(msg),
36+
Code: 1,
37+
}
38+
}
39+
40+
// exitError is an error with an exit code, that's returned when the cmd exits.
41+
// When possible, try to match these conventions: https://tldp.org/LDP/abs/html/exitcodes.html
42+
type exitError struct {
43+
Code int
44+
Err error
45+
}
46+
47+
func (e *exitError) Error() string {
48+
if e.Err == nil {
49+
return "<missing error>"
50+
}
51+
return e.Err.Error()
52+
}

cmd/root.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"context"
19+
_ "embed"
20+
"errors"
21+
"fmt"
22+
"net"
23+
"net/url"
24+
"os"
25+
"os/signal"
26+
"strconv"
27+
"strings"
28+
"syscall"
29+
30+
"cloud.google.com/go/cloudsqlconn"
31+
"github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql"
32+
"github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy"
33+
"github.com/spf13/cobra"
34+
)
35+
36+
var (
37+
// versionString indicates the version of this library.
38+
//go:embed version.txt
39+
versionString string
40+
userAgent string
41+
)
42+
43+
func init() {
44+
versionString = strings.TrimSpace(versionString)
45+
userAgent = "cloud-sql-auth-proxy/" + versionString
46+
}
47+
48+
// Execute adds all child commands to the root command and sets flags appropriately.
49+
// This is called by main.main(). It only needs to happen once to the rootCmd.
50+
func Execute() {
51+
if err := NewCommand().Execute(); err != nil {
52+
exit := 1
53+
if terr, ok := err.(*exitError); ok {
54+
exit = terr.Code
55+
}
56+
os.Exit(exit)
57+
}
58+
}
59+
60+
// Command represents an invocation of the Cloud SQL Auth Proxy.
61+
type Command struct {
62+
*cobra.Command
63+
conf *proxy.Config
64+
}
65+
66+
// Option is a function that configures a Command.
67+
type Option func(*proxy.Config)
68+
69+
// WithDialer configures the Command to use the provided dialer to connect to
70+
// Cloud SQL instances.
71+
func WithDialer(d cloudsql.Dialer) Option {
72+
return func(c *proxy.Config) {
73+
c.Dialer = d
74+
}
75+
}
76+
77+
// NewCommand returns a Command object representing an invocation of the proxy.
78+
func NewCommand(opts ...Option) *Command {
79+
c := &Command{
80+
conf: &proxy.Config{},
81+
}
82+
for _, o := range opts {
83+
o(c.conf)
84+
}
85+
86+
cmd := &cobra.Command{
87+
Use: "cloud_sql_proxy instance_connection_name...",
88+
Version: versionString,
89+
Short: "cloud_sql_proxy provides a secure way to authorize connections to Cloud SQL.",
90+
Long: `The Cloud SQL Auth proxy provides IAM-based authorization and encryption when
91+
connecting to Cloud SQL instances. It listens on a local port and forwards connections
92+
to your instance's IP address, providing a secure connection without having to manage
93+
any client SSL certificates.`,
94+
Args: func(cmd *cobra.Command, args []string) error {
95+
err := parseConfig(cmd, c.conf, args)
96+
if err != nil {
97+
return err
98+
}
99+
// The arguments are parsed. Usage is no longer needed.
100+
cmd.SilenceUsage = true
101+
return nil
102+
},
103+
RunE: func(*cobra.Command, []string) error {
104+
return runSignalWrapper(c)
105+
},
106+
}
107+
108+
// Global-only flags
109+
cmd.PersistentFlags().StringVarP(&c.conf.Token, "token", "t", "",
110+
"Bearer token used for authorization.")
111+
cmd.PersistentFlags().StringVarP(&c.conf.CredentialsFile, "credentials-file", "c", "",
112+
"Path to a service account key to use for authentication.")
113+
114+
// Global and per instance flags
115+
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
116+
"Address on which to bind Cloud SQL instance listeners.")
117+
cmd.PersistentFlags().IntVarP(&c.conf.Port, "port", "p", 0,
118+
"Initial port to use for listeners. Subsequent listeners increment from this value.")
119+
120+
c.Command = cmd
121+
return c
122+
}
123+
124+
func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
125+
// If no instance connection names were provided, error.
126+
if len(args) == 0 {
127+
return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)")
128+
}
129+
// First, validate global config.
130+
if ip := net.ParseIP(conf.Addr); ip == nil {
131+
return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr))
132+
}
133+
134+
// If both token and credentials file were set, error.
135+
if conf.Token != "" && conf.CredentialsFile != "" {
136+
return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time")
137+
}
138+
139+
switch {
140+
case conf.Token != "":
141+
cmd.Printf("Authorizing with the -token flag\n")
142+
case conf.CredentialsFile != "":
143+
cmd.Printf("Authorizing with the credentials file at %q\n", conf.CredentialsFile)
144+
default:
145+
cmd.Printf("Authorizing with Application Default Credentials")
146+
}
147+
148+
var ics []proxy.InstanceConnConfig
149+
for _, a := range args {
150+
// Assume no query params initially
151+
ic := proxy.InstanceConnConfig{
152+
Name: a,
153+
}
154+
// If there are query params, update instance config.
155+
if res := strings.SplitN(a, "?", 2); len(res) > 1 {
156+
ic.Name = res[0]
157+
q, err := url.ParseQuery(res[1])
158+
if err != nil {
159+
return newBadCommandError(fmt.Sprintf("could not parse query: %q", res[1]))
160+
}
161+
162+
if a, ok := q["address"]; ok {
163+
if len(a) != 1 {
164+
return newBadCommandError(fmt.Sprintf("address query param should be only one value: %q", a))
165+
}
166+
if ip := net.ParseIP(a[0]); ip == nil {
167+
return newBadCommandError(
168+
fmt.Sprintf("address query param is not a valid IP address: %q",
169+
a[0],
170+
))
171+
}
172+
ic.Addr = a[0]
173+
}
174+
175+
if p, ok := q["port"]; ok {
176+
if len(p) != 1 {
177+
return newBadCommandError(fmt.Sprintf("port query param should be only one value: %q", a))
178+
}
179+
pp, err := strconv.Atoi(p[0])
180+
if err != nil {
181+
return newBadCommandError(
182+
fmt.Sprintf("port query param is not a valid integer: %q",
183+
p[0],
184+
))
185+
}
186+
ic.Port = pp
187+
}
188+
}
189+
ics = append(ics, ic)
190+
}
191+
192+
conf.Instances = ics
193+
return nil
194+
}
195+
196+
// runSignalWrapper watches for SIGTERM and SIGINT and interupts execution if necessary.
197+
func runSignalWrapper(cmd *Command) error {
198+
ctx, cancel := context.WithCancel(cmd.Context())
199+
defer cancel()
200+
201+
shutdownCh := make(chan error)
202+
203+
// watch for sigterm / sigint signals
204+
signals := make(chan os.Signal, 1)
205+
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
206+
go func() {
207+
var s os.Signal
208+
select {
209+
case s = <-signals:
210+
case <-cmd.Context().Done():
211+
// this should only happen when the context supplied in tests in canceled
212+
s = syscall.SIGINT
213+
}
214+
switch s {
215+
case syscall.SIGINT:
216+
shutdownCh <- errSigInt
217+
case syscall.SIGTERM:
218+
shutdownCh <- errSigTerm
219+
}
220+
}()
221+
222+
// Start the proxy asynchronously, so we can exit early if a shutdown signal is sent
223+
startCh := make(chan *proxy.Client)
224+
go func() {
225+
defer close(startCh)
226+
// Check if the caller has configured a dialer.
227+
// Otherwise, initialize a new one.
228+
d := cmd.conf.Dialer
229+
if d == nil {
230+
opts := append(cmd.conf.DialerOpts(), cloudsqlconn.WithUserAgent(userAgent))
231+
var err error
232+
d, err = cloudsqlconn.NewDialer(ctx, opts...)
233+
if err != nil {
234+
shutdownCh <- fmt.Errorf("error initializing dialer: %v", err)
235+
return
236+
}
237+
}
238+
p, err := proxy.NewClient(ctx, d, cmd.Command, cmd.conf)
239+
if err != nil {
240+
shutdownCh <- fmt.Errorf("unable to start: %v", err)
241+
return
242+
}
243+
startCh <- p
244+
}()
245+
// Wait for either startup to finish or a signal to interupt
246+
var p *proxy.Client
247+
select {
248+
case err := <-shutdownCh:
249+
return err
250+
case p = <-startCh:
251+
}
252+
cmd.Println("The proxy has started successfully and is ready for new connections!")
253+
defer p.Close()
254+
255+
go func() {
256+
shutdownCh <- p.Serve(ctx)
257+
}()
258+
259+
err := <-shutdownCh
260+
switch {
261+
case errors.Is(err, errSigInt):
262+
cmd.PrintErrln("SIGINT signal received. Shuting down...")
263+
case errors.Is(err, errSigTerm):
264+
cmd.PrintErrln("SIGTERM signal received. Shuting down...")
265+
default:
266+
cmd.PrintErrf("The proxy has encountered a terminal error: %v\n", err)
267+
}
268+
return err
269+
}

0 commit comments

Comments
 (0)