diff --git a/api-tests/management/mysql_test.go b/api-tests/management/mysql_test.go
index 5f48073b8c..fb489c874f 100644
--- a/api-tests/management/mysql_test.go
+++ b/api-tests/management/mysql_test.go
@@ -596,6 +596,7 @@ func TestAddMySQL(t *testing.T) {
assert.Nil(t, addMySQLOK)
})
+ // according to defaultFiles parameter, passing empty username is allowed
t.Run("Empty username", func(t *testing.T) {
nodeName := pmmapitests.TestString(t, "node-name")
nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
@@ -614,11 +615,16 @@ func TestAddMySQL(t *testing.T) {
Address: "10.10.10.10",
Port: 3306,
PMMAgentID: pmmAgentID,
+
+ SkipConnectionCheck: true,
},
}
addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
- pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Username: value '' must not be an empty string")
- assert.Nil(t, addMySQLOK)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
})
t.Run("With MetricsModePush", func(t *testing.T) {
diff --git a/go.mod b/go.mod
index 2603b4c59c..d81c6be531 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
// Use for local development, but do not commit:
// replace github.com/percona/pmm => ../pmm
+// replace github.com/percona/pmm => ../pmm
// replace github.com/percona-platform/saas => ../saas
// replace github.com/percona-platform/dbaas-api => ../dbaas-api
@@ -36,7 +37,7 @@ require (
github.com/minio/minio-go/v7 v7.0.27
github.com/percona-platform/dbaas-api v0.0.0-20220110092915-5aacd784d472
github.com/percona-platform/saas v0.0.0-20220427162947-f9d246ad0f16
- github.com/percona/pmm v0.0.0-20220607154345-cf9e6085e661
+ github.com/percona/pmm v0.0.0-20220609163826-96fb5593d69b
github.com/percona/promconfig v0.2.4-0.20211110115058-98687f586f54
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.0
@@ -138,7 +139,7 @@ require (
golang.org/x/tools v0.1.9 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
- gopkg.in/ini.v1 v1.66.4 // indirect
+ gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index cf476b9637..e9ef88b99f 100644
--- a/go.sum
+++ b/go.sum
@@ -467,8 +467,8 @@ github.com/percona-platform/dbaas-api v0.0.0-20220110092915-5aacd784d472 h1:Henk
github.com/percona-platform/dbaas-api v0.0.0-20220110092915-5aacd784d472/go.mod h1:WZZ3Hi+lAWCaGWmsrfkkvRQPkIa8n1OZ0s8Su+vbgus=
github.com/percona-platform/saas v0.0.0-20220427162947-f9d246ad0f16 h1:0fx16uGtl4MwrBwm9/VSoNEhjL0cXYxS0quEhLthGcc=
github.com/percona-platform/saas v0.0.0-20220427162947-f9d246ad0f16/go.mod h1:gFUwaFp6Ugu5qsBwiOVJYbDlzgZ77tmXdXGO7tG5xVI=
-github.com/percona/pmm v0.0.0-20220607154345-cf9e6085e661 h1:xgAObd0x67UJdtEwnN5JeZFyCYQ9BCiGCnaBy4gB4F4=
-github.com/percona/pmm v0.0.0-20220607154345-cf9e6085e661/go.mod h1:ix7lLnoQysi2ki1TxfWB3GSckkvER4nVRKVHo+zkagg=
+github.com/percona/pmm v0.0.0-20220609163826-96fb5593d69b h1:b3MUqjYU1L85FExBgewV9xFluJEFRO129pRHskgxW+0=
+github.com/percona/pmm v0.0.0-20220609163826-96fb5593d69b/go.mod h1:c22C8QvyFlcxr61TbqPlLGVC2u4xDptqYuYAoV0Umbs=
github.com/percona/promconfig v0.2.4-0.20211110115058-98687f586f54 h1:aI1emmycDTGWKsBdxFPKZqohfBbK4y2ta9G4+RX7gVg=
github.com/percona/promconfig v0.2.4-0.20211110115058-98687f586f54/go.mod h1:Y2uXi5QNk71+ceJHuI9poank+0S1kjxd3K105fXKVkg=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -938,8 +938,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
-gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
+gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/reform.v1 v1.5.1 h1:7vhDFW1n1xAPC6oDSvIvVvpRkaRpXlxgJ4QB4s3aDdo=
gopkg.in/reform.v1 v1.5.1/go.mod h1:AIv0CbDRJ0ljQwptGeaIXfpDRo02uJwTq92aMFELEeU=
gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg=
diff --git a/main.go b/main.go
index 85726e74a4..35b5a1d4d8 100644
--- a/main.go
+++ b/main.go
@@ -138,6 +138,7 @@ type gRPCServerDeps struct {
actions *agents.ActionsService
agentsStateUpdater *agents.StateUpdater
connectionCheck *agents.ConnectionChecker
+ defaultsFileParser *agents.DefaultsFileParser
grafanaClient *grafana.Client
checksService *checks.Service
dbaasClient *dbaas.Client
@@ -190,7 +191,7 @@ func runGRPCServer(ctx context.Context, deps *gRPCServerDeps) {
nodeSvc := management.NewNodeService(deps.db)
serviceSvc := management.NewServiceService(deps.db, deps.agentsStateUpdater, deps.vmdb)
- mysqlSvc := management.NewMySQLService(deps.db, deps.agentsStateUpdater, deps.connectionCheck, deps.versionCache)
+ mysqlSvc := management.NewMySQLService(deps.db, deps.agentsStateUpdater, deps.connectionCheck, deps.versionCache, deps.defaultsFileParser)
mongodbSvc := management.NewMongoDBService(deps.db, deps.agentsStateUpdater, deps.connectionCheck)
postgresqlSvc := management.NewPostgreSQLService(deps.db, deps.agentsStateUpdater, deps.connectionCheck)
proxysqlSvc := management.NewProxySQLService(deps.db, deps.agentsStateUpdater, deps.connectionCheck)
@@ -737,6 +738,7 @@ func main() {
schedulerService := scheduler.New(db, backupService)
versionCache := versioncache.New(db, versioner)
emailer := alertmanager.NewEmailer(logrus.WithField("component", "alertmanager-emailer").Logger)
+ defaultsFileParser := agents.NewDefaultsFileParser(agentsRegistry)
serverParams := &server.Params{
DB: db,
@@ -937,6 +939,7 @@ func main() {
versionCache: versionCache,
supervisord: supervisord,
config: &cfg.Config,
+ defaultsFileParser: defaultsFileParser,
})
}()
diff --git a/models/defaults_file.go b/models/defaults_file.go
new file mode 100644
index 0000000000..09f138bca0
--- /dev/null
+++ b/models/defaults_file.go
@@ -0,0 +1,26 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package models
+
+// ParseDefaultsFileResult contains result of parsing defaults file.
+type ParseDefaultsFileResult struct {
+ Username string
+ Password string
+ Host string
+ Port uint32
+ Socket string
+}
diff --git a/services/agents/channel/channel.go b/services/agents/channel/channel.go
index 99ba8dfcb8..131bf66f54 100644
--- a/services/agents/channel/channel.go
+++ b/services/agents/channel/channel.go
@@ -275,6 +275,8 @@ func (c *Channel) runReceiver() {
c.publish(msg.Id, msg.Status, p.GetVersions)
case *agentpb.AgentMessage_PbmSwitchPitr:
c.publish(msg.Id, msg.Status, p.PbmSwitchPitr)
+ case *agentpb.AgentMessage_ParseDefaultsFile:
+ c.publish(msg.Id, msg.Status, p.ParseDefaultsFile)
case nil:
c.cancel(msg.Id, errors.Errorf("unimplemented: failed to handle received message %s", msg))
diff --git a/services/agents/channel/channel_test.go b/services/agents/channel/channel_test.go
index a777a641ad..57354dc93a 100644
--- a/services/agents/channel/channel_test.go
+++ b/services/agents/channel/channel_test.go
@@ -356,3 +356,50 @@ func TestUnexpectedResponsePayloadFromAgent(t *testing.T) {
close(stopServer)
<-stop
}
+
+func TestChannelForDefaultsFileParser(t *testing.T) {
+ const count = 50
+ require.True(t, count > agentRequestsCap)
+
+ connect := func(ch *Channel) error {
+ testValue := "test"
+ testPort := uint32(123123)
+ for i := uint32(1); i <= count; i++ {
+ resp, err := ch.SendAndWaitResponse(&agentpb.ParseDefaultsFileRequest{})
+ assert.NotNil(t, resp)
+ parserResponse := resp.(*agentpb.ParseDefaultsFileResponse)
+ assert.Equal(t, parserResponse.Username, testValue)
+ assert.Equal(t, parserResponse.Password, testValue)
+ assert.Equal(t, parserResponse.Socket, testValue)
+ assert.Equal(t, parserResponse.Port, testPort)
+ assert.NoError(t, err)
+ }
+
+ assert.Nil(t, <-ch.Requests())
+ return nil
+ }
+
+ stream, _, teardown := setup(t, connect, io.EOF) // EOF = server exits from handler
+ defer teardown(t)
+
+ for i := uint32(1); i <= count; i++ {
+ msg, err := stream.Recv()
+ assert.NoError(t, err)
+ assert.Equal(t, i, msg.Id)
+ assert.NotNil(t, msg.GetParseDefaultsFile())
+
+ err = stream.Send(&agentpb.AgentMessage{
+ Id: i,
+ Payload: (&agentpb.ParseDefaultsFileResponse{
+ Username: "test",
+ Password: "test",
+ Port: 123123,
+ Socket: "test",
+ }).AgentMessageResponsePayload(),
+ })
+ assert.NoError(t, err)
+ }
+
+ err := stream.CloseSend()
+ assert.NoError(t, err)
+}
diff --git a/services/agents/parse_defaults_file.go b/services/agents/parse_defaults_file.go
new file mode 100644
index 0000000000..b42e4cef2a
--- /dev/null
+++ b/services/agents/parse_defaults_file.go
@@ -0,0 +1,98 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package agents
+
+import (
+ "context"
+ "time"
+
+ "github.com/percona/pmm/api/agentpb"
+ "github.com/percona/pmm/api/inventorypb"
+ "github.com/pkg/errors"
+
+ "github.com/percona/pmm-managed/models"
+ "github.com/percona/pmm-managed/utils/logger"
+)
+
+// DefaultsFileParser requests from agent to parse defaultsFile.
+type DefaultsFileParser struct {
+ r *Registry
+}
+
+// NewDefaultsFileParser creates new ParseDefaultsFile request.
+func NewDefaultsFileParser(r *Registry) *DefaultsFileParser {
+ return &DefaultsFileParser{
+ r: r,
+ }
+}
+
+// ParseDefaultsFile sends request (with file path) to pmm-agent to parse defaults file.
+func (p *DefaultsFileParser) ParseDefaultsFile(ctx context.Context, pmmAgentID, filePath string, serviceType models.ServiceType) (*models.ParseDefaultsFileResult, error) {
+ l := logger.Get(ctx)
+
+ pmmAgent, err := p.r.get(pmmAgentID)
+ if err != nil {
+ return nil, err
+ }
+
+ start := time.Now()
+ defer func() {
+ if dur := time.Since(start); dur > 5*time.Second {
+ l.Warnf("ParseDefaultsFile took %s.", dur)
+ }
+ }()
+
+ request, err := createRequest(filePath, serviceType)
+ if err != nil {
+ l.Debugf("can't create ParseDefaultsFileRequest %s", err)
+ return nil, err
+ }
+
+ resp, err := pmmAgent.channel.SendAndWaitResponse(request)
+ if err != nil {
+ return nil, err
+ }
+
+ l.Infof("ParseDefaultsFile response from agent: %+v.", resp)
+ parserResponse, ok := resp.(*agentpb.ParseDefaultsFileResponse)
+ if !ok {
+ return nil, errors.New("wrong response from agent (not ParseDefaultsFileResponse model)")
+ }
+ if parserResponse.Error != "" {
+ return nil, errors.New(parserResponse.Error)
+ }
+
+ return &models.ParseDefaultsFileResult{
+ Username: parserResponse.Username,
+ Password: parserResponse.Password,
+ Host: parserResponse.Host,
+ Port: parserResponse.Port,
+ Socket: parserResponse.Socket,
+ }, nil
+}
+
+func createRequest(configPath string, serviceType models.ServiceType) (*agentpb.ParseDefaultsFileRequest, error) {
+ if serviceType == models.MySQLServiceType {
+ request := &agentpb.ParseDefaultsFileRequest{
+ ServiceType: inventorypb.ServiceType_MYSQL_SERVICE,
+ ConfigPath: configPath,
+ }
+ return request, nil
+ } else {
+ return nil, errors.Errorf("unhandled service type %s", serviceType)
+ }
+}
diff --git a/services/agents/parse_defaults_file_test.go b/services/agents/parse_defaults_file_test.go
new file mode 100644
index 0000000000..0f2e241479
--- /dev/null
+++ b/services/agents/parse_defaults_file_test.go
@@ -0,0 +1,41 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package agents
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/percona/pmm-managed/models"
+)
+
+func TestCreateRequest(t *testing.T) {
+ t.Parallel()
+ response, err := createRequest("/path/to/file", models.MySQLServiceType)
+
+ require.NoError(t, err)
+ require.NotNil(t, response, "ParseDefaultsFileRequest is nil")
+}
+
+func TestCreateRequestNotSupported(t *testing.T) {
+ t.Parallel()
+ response, err := createRequest("/path/to/file", models.PostgreSQLServiceType)
+
+ require.Error(t, err)
+ require.Nil(t, response, "ParseDefaultsFileRequest is not nil")
+}
diff --git a/services/management/deps.go b/services/management/deps.go
index efa1991791..832341d41e 100644
--- a/services/management/deps.go
+++ b/services/management/deps.go
@@ -34,6 +34,8 @@ import (
//go:generate mockery -name=grafanaClient -case=snake -inpkg -testonly
//go:generate mockery -name=jobsService -case=snake -inpkg -testonly
//go:generate mockery -name=connectionChecker -case=snake -inpkg -testonly
+//go:generate mockery -name=defaultsFileParser -case=snake -inpkg -testonly
+//go:generate mockery -name=versionCache -case=snake -inpkg -testonly
// agentsRegistry is a subset of methods of agents.Registry used by this package.
// We use it instead of real type for testing and to avoid dependency cycle.
@@ -93,3 +95,9 @@ type connectionChecker interface {
type versionCache interface {
RequestSoftwareVersionsUpdate()
}
+
+// defaultsFileParser is a subset of methods of agents.ParseDefaultsFile.
+// We use it instead of real type for testing and to avoid dependency cycle.
+type defaultsFileParser interface {
+ ParseDefaultsFile(ctx context.Context, pmmAgentID, filePath string, serviceType models.ServiceType) (*models.ParseDefaultsFileResult, error)
+}
diff --git a/services/management/mock_defaults_file_parser_test.go b/services/management/mock_defaults_file_parser_test.go
new file mode 100644
index 0000000000..04cf1b3e21
--- /dev/null
+++ b/services/management/mock_defaults_file_parser_test.go
@@ -0,0 +1,39 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package management
+
+import (
+ context "context"
+
+ mock "github.com/stretchr/testify/mock"
+
+ models "github.com/percona/pmm-managed/models"
+)
+
+// mockDefaultsFileParser is an autogenerated mock type for the defaultsFileParser type
+type mockDefaultsFileParser struct {
+ mock.Mock
+}
+
+// ParseDefaultsFile provides a mock function with given fields: ctx, pmmAgentID, filePath, serviceType
+func (_m *mockDefaultsFileParser) ParseDefaultsFile(ctx context.Context, pmmAgentID string, filePath string, serviceType models.ServiceType) (*models.ParseDefaultsFileResult, error) {
+ ret := _m.Called(ctx, pmmAgentID, filePath, serviceType)
+
+ var r0 *models.ParseDefaultsFileResult
+ if rf, ok := ret.Get(0).(func(context.Context, string, string, models.ServiceType) *models.ParseDefaultsFileResult); ok {
+ r0 = rf(ctx, pmmAgentID, filePath, serviceType)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*models.ParseDefaultsFileResult)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, string, string, models.ServiceType) error); ok {
+ r1 = rf(ctx, pmmAgentID, filePath, serviceType)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
diff --git a/services/management/mock_version_cache_test.go b/services/management/mock_version_cache_test.go
new file mode 100644
index 0000000000..063a059b57
--- /dev/null
+++ b/services/management/mock_version_cache_test.go
@@ -0,0 +1,15 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package management
+
+import mock "github.com/stretchr/testify/mock"
+
+// mockVersionCache is an autogenerated mock type for the versionCache type
+type mockVersionCache struct {
+ mock.Mock
+}
+
+// RequestSoftwareVersionsUpdate provides a mock function with given fields:
+func (_m *mockVersionCache) RequestSoftwareVersionsUpdate() {
+ _m.Called()
+}
diff --git a/services/management/mysql.go b/services/management/mysql.go
index ddf6723d96..efa8e2de9b 100644
--- a/services/management/mysql.go
+++ b/services/management/mysql.go
@@ -18,10 +18,13 @@ package management
import (
"context"
+ "fmt"
"github.com/AlekSi/pointer"
"github.com/percona/pmm/api/inventorypb"
"github.com/percona/pmm/api/managementpb"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
"gopkg.in/reform.v1"
"github.com/percona/pmm-managed/models"
@@ -39,15 +42,17 @@ type MySQLService struct {
state agentsStateUpdater
cc connectionChecker
vc versionCache
+ dfp defaultsFileParser
}
// NewMySQLService creates new MySQL Management Service.
-func NewMySQLService(db *reform.DB, state agentsStateUpdater, cc connectionChecker, vc versionCache) *MySQLService {
+func NewMySQLService(db *reform.DB, state agentsStateUpdater, cc connectionChecker, vc versionCache, dfp defaultsFileParser) *MySQLService {
return &MySQLService{
db: db,
state: state,
cc: cc,
vc: vc,
+ dfp: dfp,
}
}
@@ -78,6 +83,35 @@ func (s *MySQLService) Add(ctx context.Context, req *managementpb.AddMySQLReques
if err != nil {
return err
}
+
+ if req.DefaultsFile != "" {
+ result, err := s.dfp.ParseDefaultsFile(ctx, req.PmmAgentId, req.DefaultsFile, models.MySQLServiceType)
+ if err != nil {
+ return status.Error(codes.FailedPrecondition, fmt.Sprintf("Defaults file error: %s.", err))
+ }
+
+ // set username and password from parsed defaults file by agent
+ if req.Username == "" && result.Username != "" {
+ req.Username = result.Username
+ }
+
+ if req.Password == "" && result.Password != "" {
+ req.Password = result.Password
+ }
+
+ if req.Address == "" && result.Host != "" {
+ req.Address = result.Host
+ }
+
+ if req.Port == 0 && result.Port > 0 {
+ req.Port = result.Port
+ }
+
+ if req.Socket == "" && result.Socket != "" {
+ req.Socket = result.Socket
+ }
+ }
+
service, err := models.AddNewService(tx.Querier, models.MySQLServiceType, &models.AddDBMSServiceParams{
ServiceName: req.ServiceName,
NodeID: nodeID,
diff --git a/services/management/mysql_test.go b/services/management/mysql_test.go
new file mode 100644
index 0000000000..c27d145bd6
--- /dev/null
+++ b/services/management/mysql_test.go
@@ -0,0 +1,152 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/percona/pmm/api/managementpb"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "gopkg.in/reform.v1"
+ "gopkg.in/reform.v1/dialects/postgresql"
+
+ "github.com/percona/pmm-managed/models"
+ "github.com/percona/pmm-managed/utils/logger"
+ "github.com/percona/pmm-managed/utils/testdb"
+ "github.com/percona/pmm-managed/utils/tests"
+)
+
+func setup(t *testing.T) (*MySQLService, func(t *testing.T), context.Context) {
+ t.Helper()
+
+ uuid.SetRand(&tests.IDReader{})
+
+ sqlDB := testdb.Open(t, models.SetupFixtures, nil)
+ db := reform.NewDB(sqlDB, postgresql.Dialect, reform.NewPrintfLogger(t.Logf))
+
+ agentsRegistry := &mockAgentsRegistry{}
+ agentsRegistry.Test(t)
+
+ agentsStateUpdater := &mockAgentsStateUpdater{}
+ agentsStateUpdater.Test(t)
+
+ connectionChecker := &mockConnectionChecker{}
+ connectionChecker.Test(t)
+
+ defaultsFileParser := &mockDefaultsFileParser{}
+ defaultsFileParser.Test(t)
+
+ versionCache := &mockVersionCache{}
+ versionCache.Test(t)
+
+ teardown := func(t *testing.T) {
+ uuid.SetRand(nil)
+
+ require.NoError(t, sqlDB.Close())
+
+ agentsRegistry.AssertExpectations(t)
+ agentsStateUpdater.AssertExpectations(t)
+ connectionChecker.AssertExpectations(t)
+ defaultsFileParser.AssertExpectations(t)
+ versionCache.AssertExpectations(t)
+ }
+
+ return NewMySQLService(db, agentsStateUpdater, connectionChecker, versionCache, defaultsFileParser),
+ teardown,
+ logger.Set(context.Background(), t.Name())
+}
+
+func TestDefaultsFileParserRequest(t *testing.T) {
+ t.Run("Add MySQLService defaults file test", func(t *testing.T) {
+ service, teardown, ctx := setup(t)
+ defer teardown(t)
+
+ service.state.(*mockAgentsStateUpdater).On("RequestStateUpdate", ctx, "pmm-server").Once()
+ service.vc.(*mockVersionCache).On("RequestSoftwareVersionsUpdate").Once()
+ service.dfp.(*mockDefaultsFileParser).On("ParseDefaultsFile", ctx, mock.Anything, "/file/path", models.MySQLServiceType).Return(&models.ParseDefaultsFileResult{
+ Username: "test",
+ Password: "test",
+ Host: "192.168.2.1",
+ Port: 6666,
+ }, nil).Once()
+
+ req := &managementpb.AddMySQLRequest{
+ NodeId: models.PMMServerNodeID,
+ PmmAgentId: models.PMMServerAgentID,
+ ServiceName: "test",
+ DefaultsFile: "/file/path",
+ SkipConnectionCheck: true,
+ }
+ res, err := service.Add(ctx, req)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+
+ require.Equal(t, res.Service.Address, "192.168.2.1")
+ require.Equal(t, res.Service.Port, uint32(6666))
+ require.Equal(t, res.MysqldExporter.Username, "test")
+ })
+
+ t.Run("Add MySQLService defaults file with overriden params", func(t *testing.T) {
+ service, teardown, ctx := setup(t)
+ defer teardown(t)
+
+ service.state.(*mockAgentsStateUpdater).On("RequestStateUpdate", ctx, "pmm-server").Once()
+ service.vc.(*mockVersionCache).On("RequestSoftwareVersionsUpdate").Once()
+ service.dfp.(*mockDefaultsFileParser).On("ParseDefaultsFile", ctx, mock.Anything, "/file/path", models.MySQLServiceType).Return(&models.ParseDefaultsFileResult{
+ Username: "test",
+ Socket: "socks4://localhost",
+ }, nil).Once()
+
+ req := &managementpb.AddMySQLRequest{
+ NodeId: models.PMMServerNodeID,
+ PmmAgentId: models.PMMServerAgentID,
+ ServiceName: "test overriden",
+ DefaultsFile: "/file/path",
+ Username: "overriden",
+ SkipConnectionCheck: true,
+ }
+ res, err := service.Add(ctx, req)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+
+ require.Equal(t, res.Service.Socket, "socks4://localhost")
+ require.Equal(t, res.MysqldExporter.Username, "overriden")
+ })
+
+ t.Run("Add MySQLService defaults file parse error", func(t *testing.T) {
+ service, teardown, ctx := setup(t)
+ defer teardown(t)
+
+ service.dfp.(*mockDefaultsFileParser).On("ParseDefaultsFile", ctx, mock.Anything, "/file/path", models.MySQLServiceType).Return(nil, errors.New("dfp error")).Once()
+
+ req := &managementpb.AddMySQLRequest{
+ NodeId: models.PMMServerNodeID,
+ PmmAgentId: models.PMMServerAgentID,
+ ServiceName: "not used",
+ DefaultsFile: "/file/path",
+ Username: "overriden",
+ SkipConnectionCheck: true,
+ }
+ res, err := service.Add(ctx, req)
+ require.Error(t, err, "dfp error")
+ require.Nil(t, res)
+ })
+}