diff --git a/cmd/options.go b/cmd/options.go new file mode 100644 index 000000000..6536ba0e4 --- /dev/null +++ b/cmd/options.go @@ -0,0 +1,84 @@ +// 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(max uint64) Option { + return func(c *Command) { + c.conf.MaxConnections = max + } +} + +// 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 + } +} diff --git a/cmd/options_test.go b/cmd/options_test.go new file mode 100644 index 000000000..01506f0d5 --- /dev/null +++ b/cmd/options_test.go @@ -0,0 +1,213 @@ +// 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(), + }, + } + + 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 index 5ad7693d1..4b50d7c3b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -91,24 +91,6 @@ type Command struct { cleanup func() error } -// 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 - } -} - var longHelp = ` Overview @@ -397,10 +379,6 @@ func NewCommand(opts ...Option) *Command { UserAgent: userAgent, }, } - for _, o := range opts { - o(c) - } - var waitCmd = &cobra.Command{ Use: "wait", RunE: runWaitCmd, @@ -418,6 +396,10 @@ func NewCommand(opts ...Option) *Command { if len(args) == 0 { args = instanceFromEnv(args) } + // Override CLI based arguments with any programmatic options. + for _, o := range opts { + o(c) + } // Handle logger separately from config if c.conf.StructuredLogs { c.logger, c.cleanup = log.NewStructuredLogger(c.conf.Quiet)