-
Notifications
You must be signed in to change notification settings - Fork 350
/
root.go
1199 lines (1031 loc) · 40.4 KB
/
root.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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
}
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. Optionally supports a min-ready query param (e.g.,
/readiness?min-ready=3) where the Proxy will return a 200 status if the
Proxy can connect successfully to at least min-ready number of instances. If
min-ready exceeds the number of registered instances, returns a 400.
- /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 <INSTANCE_CONNECTION_NAME> --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.11.4/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 enabled. If an alternate health
check port or address is used, as in:
./cloud-sql-proxy <INSTANCE_CONNECTION_NAME> \
--http-address 0.0.0.0 \
--http-port 9191
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
`
const (
waitMaxFlag = "max"
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
}
}
}
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)
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.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, "admin-port", "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 TPC 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.`)
// 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, c.cleanup = 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() {