diff --git a/commands/base.go b/commands/base.go index 497715dd..8763b571 100644 --- a/commands/base.go +++ b/commands/base.go @@ -25,11 +25,15 @@ import ( "io/ioutil" "net/http" "net/url" + "os/user" + "path/filepath" "reflect" "regexp" "strings" "text/template" + "gopkg.in/ini.v1" + "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" inventorypb "github.com/percona/pmm/api/inventorypb/json/client" @@ -69,6 +73,10 @@ type Command interface { Run() (Result, error) } +type ApplyDefaults interface { + ApplyDefaults(cfg *ini.File) +} + // TODO remove Command above, rename CommandWithContext to Command type CommandWithContext interface { // TODO rename to Run @@ -115,6 +123,7 @@ type globalFlagsValues struct { ServerInsecureTLS bool Debug bool Trace bool + DefaultConfig string } // GlobalFlags contains pmm-admin core flags values. @@ -184,6 +193,37 @@ func (e errFromNginx) GoString() string { return fmt.Sprintf("errFromNginx(%q)", string(e)) } +func ConfigureDefaults(config string, cmd ApplyDefaults) error { + if config != "" { + var err error + config, err = expandPath(config) + if err != nil { + return fmt.Errorf("fail to normalize path: %v", err) + } + cfg, err := ini.Load(config) + if err != nil { + return fmt.Errorf("fail to read config file: %v", err) + } + + cmd.ApplyDefaults(cfg) + } else { + logrus.Debug("default config not provided") + } + + return nil +} + +func expandPath(path string) (string, error) { + if strings.HasPrefix(path, "~/") { + usr, err := user.Current() + if err != nil { + return "", err + } + return filepath.Join(usr.HomeDir, path[2:]), nil + } + return path, nil +} + // SetupClients configures local and PMM Server API clients. func SetupClients(ctx context.Context, serverURL string) { agentlocal.SetTransport(ctx, GlobalFlags.Debug || GlobalFlags.Trace) diff --git a/commands/base_test.go b/commands/base_test.go index 9dc938d9..61d4f1ee 100644 --- a/commands/base_test.go +++ b/commands/base_test.go @@ -20,9 +20,12 @@ import ( "fmt" "io/ioutil" "os" + "os/user" "strings" "testing" + "gopkg.in/ini.v1" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -110,3 +113,63 @@ func TestReadFile(t *testing.T) { require.Empty(t, certificate) }) } + +type cmdWithDefaultsApply struct { + applyDefaultCalled bool + user string + password string +} + +func (c *cmdWithDefaultsApply) ApplyDefaults(cfg *ini.File) { + c.user = cfg.Section("client").Key("user").String() + c.password = cfg.Section("client").Key("password").String() + c.applyDefaultCalled = true +} + +func TestConfigureDefaults(t *testing.T) { + t.Run("ApplyDefaults is called if command supports it", func(t *testing.T) { + file, e := os.Open("../testdata/.my.cnf") + if e != nil { + t.Fatal(e) + } + + cmd := &cmdWithDefaultsApply{} + + if err := ConfigureDefaults(file.Name(), cmd); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "root", cmd.user) + assert.Equal(t, "toor", cmd.password) + }) + + t.Run("ApplyDefaults is not called if pass is not setup", func(t *testing.T) { + cmd := &cmdWithDefaultsApply{} + + if err := ConfigureDefaults("", cmd); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "", cmd.user) + assert.Equal(t, "", cmd.password) + assert.False(t, cmd.applyDefaultCalled) + }) +} + +func TestExpandPath(t *testing.T) { + t.Run("relative to userhome", func(t *testing.T) { + actual, err := expandPath("~/") + assert.NoError(t, err) + usr, err := user.Current() + assert.NoError(t, err) + + assert.Equal(t, usr.HomeDir, actual) + }) + t.Run("relative to userhome", func(t *testing.T) { + originalPath := "./test" + actual, err := expandPath(originalPath) + assert.NoError(t, err) + + assert.Equal(t, originalPath, actual) + }) +} diff --git a/commands/default_config.go b/commands/default_config.go new file mode 100644 index 00000000..8ed1fd79 --- /dev/null +++ b/commands/default_config.go @@ -0,0 +1,41 @@ +// pmm-admin +// Copyright 2019 Percona 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 commands + +import ( + "io/ioutil" + "os" +) + +func DefaultConfig(val string) (f *os.File, cleanup func(), e error) { + file, err := ioutil.TempFile("", "test-pmm-admin-defaults-*.cnf") + if err != nil { + return nil, nil, err + } + f = file + cleanup = func() { + _ = os.Remove(file.Name()) + } + + if _, err := file.WriteString(val); err != nil { + return nil, nil, err + } + if err := file.Sync(); err != nil { + return nil, nil, err + } + + return +} diff --git a/commands/management/add_mysql.go b/commands/management/add_mysql.go index 9327812e..501fdcf7 100644 --- a/commands/management/add_mysql.go +++ b/commands/management/add_mysql.go @@ -21,12 +21,17 @@ import ( "strconv" "strings" + "github.com/sirupsen/logrus" + + "gopkg.in/ini.v1" + + "github.com/percona/pmm-admin/agentlocal" + "github.com/AlekSi/pointer" "github.com/alecthomas/units" "github.com/percona/pmm/api/managementpb/json/client" mysql "github.com/percona/pmm/api/managementpb/json/client/my_sql" - "github.com/percona/pmm-admin/agentlocal" "github.com/percona/pmm-admin/commands" ) @@ -116,6 +121,25 @@ type addMySQLCommand struct { CreateUser bool } +func (cmd *addMySQLCommand) ApplyDefaults(cfg *ini.File) { + defaultUsername := cfg.Section("client").Key("user").String() + if defaultUsername != "" { + if cmd.Username == "" { + cmd.Username = defaultUsername + } else { + logrus.Debug("default username is not used, it is already set") + } + } + defaultPassword := cfg.Section("client").Key("password").String() + if defaultPassword != "" { + if cmd.Password == "" { + cmd.Password = defaultPassword + } else { + logrus.Debug("default password is not used, it is already set") + } + } +} + func (cmd *addMySQLCommand) GetServiceName() string { return cmd.ServiceName } diff --git a/commands/management/add_mysql_test.go b/commands/management/add_mysql_test.go index b193c2f7..0f58c44d 100644 --- a/commands/management/add_mysql_test.go +++ b/commands/management/add_mysql_test.go @@ -19,6 +19,10 @@ import ( "strings" "testing" + "github.com/sirupsen/logrus" + + "github.com/percona/pmm-admin/commands" + mysql "github.com/percona/pmm/api/managementpb/json/client/my_sql" "github.com/stretchr/testify/assert" ) @@ -160,3 +164,87 @@ func TestRun(t *testing.T) { } }) } + +func TestApplyDefaults(t *testing.T) { + t.Run("password and username is set", func(t *testing.T) { + file, cleanup, e := commands.DefaultConfig("[client]\nuser=root\npassword=toor\n") + if e != nil { + t.Fatal(e) + } + defer cleanup() + + cmd := &addMySQLCommand{} + + commands.ConfigureDefaults(file.Name(), cmd) + + assert.Equal(t, "root", cmd.Username) + assert.Equal(t, "toor", cmd.Password) + }) + + t.Run("password and username from config have lower priority", func(t *testing.T) { + logrus.SetLevel(logrus.TraceLevel) + file, cleanup, e := commands.DefaultConfig("[client]\nuser=root\npassword=toor\n") + if e != nil { + t.Fatal(e) + } + defer cleanup() + + cmd := &addMySQLCommand{ + Username: "default-username", + Password: "default-password", + } + + commands.ConfigureDefaults(file.Name(), cmd) + + assert.Equal(t, "default-username", cmd.Username) + assert.Equal(t, "default-password", cmd.Password) + }) + + t.Run("not updated if not set", func(t *testing.T) { + file, cleanup, e := commands.DefaultConfig("") + if e != nil { + t.Fatal(e) + } + defer cleanup() + + cmd := &addMySQLCommand{ + Username: "default-username", + Password: "default-password", + } + + commands.ConfigureDefaults(file.Name(), cmd) + + assert.Equal(t, "default-username", cmd.Username) + assert.Equal(t, "default-password", cmd.Password) + }) + + t.Run("only username is set", func(t *testing.T) { + file, cleanup, e := commands.DefaultConfig("[client]\nuser=root\n") + if e != nil { + t.Fatal(e) + } + defer cleanup() + + cmd := &addMySQLCommand{} + + commands.ConfigureDefaults(file.Name(), cmd) + + assert.Equal(t, "root", cmd.Username) + assert.Equal(t, "", cmd.Password) + }) + + t.Run("only password is set", func(t *testing.T) { + file, cleanup, e := commands.DefaultConfig("[client]\npassword=toor\n") + if e != nil { + t.Fatal(e) + } + defer cleanup() + + cmd := &addMySQLCommand{} + + commands.ConfigureDefaults(file.Name(), cmd) + + assert.Equal(t, "", cmd.Username) + assert.Equal(t, "toor", cmd.Password) + }) +} diff --git a/go.mod b/go.mod index cc5a61bb..a9d23b0c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/stretchr/testify v1.6.1 golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6 gopkg.in/alecthomas/kingpin.v2 v2.2.6 + gopkg.in/ini.v1 v1.63.2 ) require ( diff --git a/go.sum b/go.sum index 531772d9..1903bfbc 100644 --- a/go.sum +++ b/go.sum @@ -387,6 +387,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/reform.v1 v1.5.0/go.mod h1:AIv0CbDRJ0ljQwptGeaIXfpDRo02uJwTq92aMFELEeU= 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= diff --git a/main.go b/main.go index 6a88641e..ec4cd5a8 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,8 @@ func main() { serverURLF := kingpin.Flag("server-url", "PMM Server URL in `https://username:password@pmm-server-host/` format").String() kingpin.Flag("server-insecure-tls", "Skip PMM Server TLS certificate validation").BoolVar(&commands.GlobalFlags.ServerInsecureTLS) + kingpin.Flag("defaults-file", "Default config").StringVar(&commands.GlobalFlags.DefaultConfig) + kingpin.Flag("debug", "Enable debug logging").BoolVar(&commands.GlobalFlags.Debug) kingpin.Flag("trace", "Enable trace logging (implies debug)").BoolVar(&commands.GlobalFlags.Trace) jsonF := kingpin.Flag("json", "Enable JSON output").Bool() @@ -152,6 +154,13 @@ func main() { commands.SetupClients(ctx, *serverURLF) } + if c, ok := command.(commands.ApplyDefaults); ok { + err := commands.ConfigureDefaults(commands.GlobalFlags.DefaultConfig, c) + if err != nil { + logrus.Panicf("Failed to configure defaults: %v", err) + } + } + var res commands.Result var err error if cc, ok := command.(commands.CommandWithContext); ok { diff --git a/testdata/.my.cnf b/testdata/.my.cnf new file mode 100644 index 00000000..d46f6bfe --- /dev/null +++ b/testdata/.my.cnf @@ -0,0 +1,5 @@ +[client] +host=127.0.0.1 +port=12345 +user=root +password=toor