diff --git a/.mockery.yaml b/.mockery.yaml index 45f9648cd0..4e71fdc67d 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -38,7 +38,7 @@ packages: interfaces: agentsRegistry: agentsStateUpdater: - apiKeyProvider: + authProvider: checksService: connectionChecker: grafanaClient: diff --git a/admin/commands/base/setup.go b/admin/commands/base/setup.go index a7dc9a472f..030030f755 100644 --- a/admin/commands/base/setup.go +++ b/admin/commands/base/setup.go @@ -88,8 +88,13 @@ func SetupClients(ctx context.Context, globalFlags *flags.GlobalFlags) { // use JSON APIs over HTTP/1.1 transport := httptransport.New(globalFlags.ServerURL.Host, globalFlags.ServerURL.Path, []string{globalFlags.ServerURL.Scheme}) if u := globalFlags.ServerURL.User; u != nil { + user := u.Username() password, _ := u.Password() - transport.DefaultAuthentication = httptransport.BasicAuth(u.Username(), password) + if user == "service_token" || user == "api_key" { + transport.DefaultAuthentication = httptransport.BearerToken(password) + } else { + transport.DefaultAuthentication = httptransport.BasicAuth(user, password) + } } transport.SetLogger(logrus.WithField("component", "server-transport")) transport.SetDebug(globalFlags.EnableDebug || globalFlags.EnableTrace) diff --git a/admin/commands/management/unregister.go b/admin/commands/management/unregister.go index 1bd1292057..b637c1f997 100644 --- a/admin/commands/management/unregister.go +++ b/admin/commands/management/unregister.go @@ -16,12 +16,15 @@ package management import ( "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/percona/pmm/admin/agentlocal" "github.com/percona/pmm/admin/commands" "github.com/percona/pmm/admin/helpers" - "github.com/percona/pmm/api/inventorypb/json/client" + inventoryClient "github.com/percona/pmm/api/inventorypb/json/client" "github.com/percona/pmm/api/inventorypb/json/client/nodes" + "github.com/percona/pmm/api/managementpb/json/client" + "github.com/percona/pmm/api/managementpb/json/client/node" ) type unregisterResult struct { @@ -63,7 +66,7 @@ func (cmd *UnregisterCommand) RunCmd() (commands.Result, error) { } nodeID = status.NodeID - node, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{ + node, err := inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{ Context: commands.Ctx, Body: nodes.GetNodeBody{ NodeID: nodeID, @@ -78,19 +81,23 @@ func (cmd *UnregisterCommand) RunCmd() (commands.Result, error) { } } - params := &nodes.RemoveNodeParams{ - Body: nodes.RemoveNodeBody{ + params := &node.UnregisterNodeParams{ + Body: node.UnregisterNodeBody{ NodeID: nodeID, Force: cmd.Force, }, Context: commands.Ctx, } - _, err = client.Default.Nodes.RemoveNode(params) + res, err := client.Default.Node.UnregisterNode(params) if err != nil { return nil, err } + if res.Payload.Warning != "" { + logrus.Warning(res.Payload.Warning) + } + return &unregisterResult{ NodeID: nodeID, NodeName: nodeName, @@ -98,7 +105,7 @@ func (cmd *UnregisterCommand) RunCmd() (commands.Result, error) { } func nodeIDFromNodeName(nodeName string) (string, error) { - listNodes, err := client.Default.Nodes.ListNodes(nil) + listNodes, err := inventoryClient.Default.Nodes.ListNodes(nil) if err != nil { return "", err } diff --git a/agent/client/basic_auth.go b/agent/client/basic_auth.go index 2984533215..c6716a64e7 100644 --- a/agent/client/basic_auth.go +++ b/agent/client/basic_auth.go @@ -17,6 +17,7 @@ package client import ( "context" "encoding/base64" + "fmt" "google.golang.org/grpc/credentials" ) @@ -31,7 +32,7 @@ func (b *basicAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[ auth := b.username + ":" + b.password enc := base64.StdEncoding.EncodeToString([]byte(auth)) return map[string]string{ - "authorization": "Basic " + enc, + "Authorization": fmt.Sprintf("Basic %s", enc), }, nil } diff --git a/agent/client/client.go b/agent/client/client.go index b79eb8e261..8a7dfb807b 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -748,10 +748,16 @@ func dial(dialCtx context.Context, cfg *config.Config, l *logrus.Entry) (*dialRe } if cfg.Server.Username != "" { - opts = append(opts, grpc.WithPerRPCCredentials(&basicAuth{ - username: cfg.Server.Username, - password: cfg.Server.Password, - })) + if cfg.Server.Username == "service_token" || cfg.Server.Username == "api_key" { + opts = append(opts, grpc.WithPerRPCCredentials(&tokenAuth{ + token: cfg.Server.Password, + })) + } else { + opts = append(opts, grpc.WithPerRPCCredentials(&basicAuth{ + username: cfg.Server.Username, + password: cfg.Server.Password, + })) + } } l.Infof("Connecting to %s ...", cfg.Server.FilteredURL()) diff --git a/agent/client/token_auth.go b/agent/client/token_auth.go new file mode 100644 index 0000000000..2204f9cab8 --- /dev/null +++ b/agent/client/token_auth.go @@ -0,0 +1,43 @@ +// Copyright (C) 2023 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 client + +import ( + "context" + "fmt" + + "google.golang.org/grpc/credentials" +) + +type tokenAuth struct { + token string +} + +// GetRequestMetadata implements credentials.PerRPCCredentials interface. +func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { //nolint:revive + return map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", t.token), + }, nil +} + +// RequireTransportSecurity implements credentials.PerRPCCredentials interface. +func (*tokenAuth) RequireTransportSecurity() bool { + return false +} + +// check interfaces. +var ( + _ credentials.PerRPCCredentials = (*tokenAuth)(nil) +) diff --git a/agent/commands/clients.go b/agent/commands/clients.go index c44353b9e6..039d48201b 100644 --- a/agent/commands/clients.go +++ b/agent/commands/clients.go @@ -104,8 +104,13 @@ func setServerTransport(u *url.URL, insecureTLS bool, l *logrus.Entry) { // use JSON APIs over HTTP/1.1 transport := httptransport.New(u.Host, u.Path, []string{u.Scheme}) if u.User != nil { + user := u.User.Username() password, _ := u.User.Password() - transport.DefaultAuthentication = httptransport.BasicAuth(u.User.Username(), password) + if user == "service_token" || user == "api_key" { + transport.DefaultAuthentication = httptransport.BearerToken(password) + } else { + transport.DefaultAuthentication = httptransport.BasicAuth(user, password) + } } transport.SetLogger(l) transport.SetDebug(l.Logger.GetLevel() >= logrus.DebugLevel) diff --git a/agent/commands/setup.go b/agent/commands/setup.go index 0cd218f944..e0b6d4f95d 100644 --- a/agent/commands/setup.go +++ b/agent/commands/setup.go @@ -160,10 +160,10 @@ func register(cfg *config.Config, l *logrus.Entry) { } cfg.ID = agentID if token != "" { - cfg.Server.Username = "api_key" + cfg.Server.Username = "service_token" cfg.Server.Password = token } else { - l.Info("PMM Server responded with an empty api key token. Consider upgrading PMM Server to the latest version.") + l.Info("PMM Server responded with an empty service token. Consider upgrading PMM Server to the latest version.") } fmt.Printf("Registered.\n") } diff --git a/api-tests/helpers.go b/api-tests/helpers.go index e3cd51c598..89709075c2 100644 --- a/api-tests/helpers.go +++ b/api-tests/helpers.go @@ -17,8 +17,10 @@ package apitests import ( "context" + "crypto/rand" "fmt" - "math/rand" + "math" + "math/big" "reflect" "testing" @@ -26,10 +28,12 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" - "github.com/percona/pmm/api/inventorypb/json/client" + inventoryClient "github.com/percona/pmm/api/inventorypb/json/client" "github.com/percona/pmm/api/inventorypb/json/client/agents" "github.com/percona/pmm/api/inventorypb/json/client/nodes" "github.com/percona/pmm/api/inventorypb/json/client/services" + "github.com/percona/pmm/api/managementpb/json/client" + "github.com/percona/pmm/api/managementpb/json/client/node" ) // ErrorResponse represents the response structure for error scenarios. @@ -49,7 +53,10 @@ type TestingT interface { func TestString(t TestingT, name string) string { t.Helper() - n := rand.Int() //nolint:gosec + // Without proper seed parallel tests can generate same "random" number. + n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32)) + require.NoError(t, err) + return fmt.Sprintf("pmm-api-tests/%s/%s/%s/%d", Hostname, t.Name(), name, n) } @@ -130,6 +137,26 @@ func (tt *expectedFailureTestingT) Check() { tt.t.Fatalf("%s expected to fail, but didn't: %s", tt.Name(), tt.link) } +// UnregisterNodes unregister specified nodes. +func UnregisterNodes(t TestingT, nodeIDs ...string) { + t.Helper() + + for _, nodeID := range nodeIDs { + params := &node.UnregisterNodeParams{ + Body: node.UnregisterNodeBody{ + NodeID: nodeID, + }, + Context: context.Background(), + } + + res, err := client.Default.Node.UnregisterNode(params) + require.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Payload) + assert.Empty(t, res.Payload.Warning) + } +} + // RemoveNodes removes specified nodes. func RemoveNodes(t TestingT, nodeIDs ...string) { t.Helper() @@ -141,7 +168,7 @@ func RemoveNodes(t TestingT, nodeIDs ...string) { }, Context: context.Background(), } - res, err := client.Default.Nodes.RemoveNode(params) + res, err := inventoryClient.Default.Nodes.RemoveNode(params) assert.NoError(t, err) assert.NotNil(t, res) } @@ -159,7 +186,7 @@ func RemoveServices(t TestingT, serviceIDs ...string) { }, Context: context.Background(), } - res, err := client.Default.Services.RemoveService(params) + res, err := inventoryClient.Default.Services.RemoveService(params) assert.NoError(t, err) assert.NotNil(t, res) } @@ -176,7 +203,7 @@ func RemoveAgents(t TestingT, agentIDs ...string) { }, Context: context.Background(), } - res, err := client.Default.Agents.RemoveAgent(params) + res, err := inventoryClient.Default.Agents.RemoveAgent(params) assert.NoError(t, err) assert.NotNil(t, res) } @@ -193,7 +220,7 @@ func AddGenericNode(t TestingT, nodeName string) *nodes.AddGenericNodeOKBodyGene }, Context: Context, } - res, err := client.Default.Nodes.AddGenericNode(params) + res, err := inventoryClient.Default.Nodes.AddGenericNode(params) assert.NoError(t, err) require.NotNil(t, res) require.NotNil(t, res.Payload) @@ -212,7 +239,7 @@ func AddRemoteNode(t TestingT, nodeName string) *nodes.AddRemoteNodeOKBody { }, Context: Context, } - res, err := client.Default.Nodes.AddRemoteNode(params) + res, err := inventoryClient.Default.Nodes.AddRemoteNode(params) assert.NoError(t, err) require.NotNil(t, res) return res.Payload @@ -227,7 +254,7 @@ func AddNode(t TestingT, nodeBody *nodes.AddNodeBody) *nodes.AddNodeOKBody { Context: Context, } - res, err := client.Default.Nodes.AddNode(params) + res, err := inventoryClient.Default.Nodes.AddNode(params) assert.NoError(t, err) require.NotNil(t, res) @@ -238,7 +265,7 @@ func AddNode(t TestingT, nodeBody *nodes.AddNodeBody) *nodes.AddNodeOKBody { func AddPMMAgent(t TestingT, nodeID string) *agents.AddPMMAgentOKBody { t.Helper() - res, err := client.Default.Agents.AddPMMAgent(&agents.AddPMMAgentParams{ + res, err := inventoryClient.Default.Agents.AddPMMAgent(&agents.AddPMMAgentParams{ Body: agents.AddPMMAgentBody{ RunsOnNodeID: nodeID, }, diff --git a/api-tests/inventory/nodes_test.go b/api-tests/inventory/nodes_test.go index 6cd3d5c466..9bfbbcbce0 100644 --- a/api-tests/inventory/nodes_test.go +++ b/api-tests/inventory/nodes_test.go @@ -32,7 +32,6 @@ import ( ) func TestNodesDeprecated(t *testing.T) { - t.Parallel() t.Run("List", func(t *testing.T) { t.Parallel() @@ -92,7 +91,6 @@ func TestNodesDeprecated(t *testing.T) { } func TestNodes(t *testing.T) { - t.Parallel() t.Run("List", func(t *testing.T) { t.Parallel() @@ -164,7 +162,6 @@ func TestNodes(t *testing.T) { } func TestGetNode(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -218,7 +215,6 @@ func TestGetNode(t *testing.T) { } func TestGenericNodeDeprecated(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -278,7 +274,6 @@ func TestGenericNodeDeprecated(t *testing.T) { } func TestGenericNode(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -342,7 +337,6 @@ func TestGenericNode(t *testing.T) { } func TestContainerNodeDeprecated(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -407,7 +401,6 @@ func TestContainerNodeDeprecated(t *testing.T) { } func TestContainerNode(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -476,7 +469,6 @@ func TestContainerNode(t *testing.T) { } func TestRemoteNodeDeprecated(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -541,7 +533,6 @@ func TestRemoteNodeDeprecated(t *testing.T) { } func TestRemoteNode(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() @@ -610,7 +601,6 @@ func TestRemoteNode(t *testing.T) { } func TestRemoveNode(t *testing.T) { - t.Parallel() t.Run("Basic", func(t *testing.T) { t.Parallel() diff --git a/api-tests/management/backup/backups_test.go b/api-tests/management/backup/backups_test.go index 609989133d..2a10622e14 100644 --- a/api-tests/management/backup/backups_test.go +++ b/api-tests/management/backup/backups_test.go @@ -41,7 +41,7 @@ func TestScheduleBackup(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer management.RemovePMMAgentWithSubAgents(t, pmmAgentID) mongo1Name := pmmapitests.TestString(t, "mongo") mongo2Name := pmmapitests.TestString(t, "mongo") diff --git a/api-tests/management/haproxy_test.go b/api-tests/management/haproxy_test.go index 7b25a0103e..2dddae47ea 100644 --- a/api-tests/management/haproxy_test.go +++ b/api-tests/management/haproxy_test.go @@ -41,7 +41,7 @@ func TestAddHAProxy(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -109,7 +109,7 @@ func TestAddHAProxy(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -257,7 +257,7 @@ func TestAddHAProxy(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-the-same-name") diff --git a/api-tests/management/helpers.go b/api-tests/management/helpers.go index 3f00f54eb7..96d3680793 100644 --- a/api-tests/management/helpers.go +++ b/api-tests/management/helpers.go @@ -48,6 +48,7 @@ func RegisterGenericNode(t pmmapitests.TestingT, body node.RegisterNodeBody) (st require.NotNil(t, registerOK.Payload.PMMAgent.AgentID) require.NotNil(t, registerOK.Payload.GenericNode) require.NotNil(t, registerOK.Payload.GenericNode.NodeID) + return registerOK.Payload.GenericNode.NodeID, registerOK.Payload.PMMAgent.AgentID } @@ -65,6 +66,7 @@ func registerContainerNode(t pmmapitests.TestingT, body node.RegisterNodeBody) ( require.NotNil(t, registerOK.Payload.PMMAgent.AgentID) require.NotNil(t, registerOK.Payload.ContainerNode) require.NotNil(t, registerOK.Payload.ContainerNode.NodeID) + return registerOK.Payload.ContainerNode.NodeID, registerOK.Payload.PMMAgent.AgentID } diff --git a/api-tests/management/mongodb_test.go b/api-tests/management/mongodb_test.go index 17472529af..60c411a0b0 100644 --- a/api-tests/management/mongodb_test.go +++ b/api-tests/management/mongodb_test.go @@ -40,7 +40,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-basic-name") @@ -108,12 +108,12 @@ func TestAddMongoDB(t *testing.T) { }) t.Run("With agents", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "with-agents") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-all-fields") @@ -196,12 +196,12 @@ func TestAddMongoDB(t *testing.T) { }) t.Run("With labels", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "with-labels") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-all-fields") @@ -260,7 +260,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-the-same-name") @@ -306,7 +306,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name") @@ -422,7 +422,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test")) @@ -453,7 +453,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) params := &mongodb.AddMongoDBParams{ @@ -471,7 +471,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -494,7 +494,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -518,7 +518,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -542,7 +542,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -570,7 +570,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-mongo-socket-name") @@ -639,7 +639,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-basic-name") @@ -711,7 +711,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-basic-name") @@ -782,7 +782,7 @@ func TestAddMongoDB(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name-for-basic-name") @@ -884,7 +884,7 @@ func TestRemoveMongoDB(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-name") nodeName := pmmapitests.TestString(t, "node-remove-by-name") nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -915,7 +915,7 @@ func TestRemoveMongoDB(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-id") nodeName := pmmapitests.TestString(t, "node-remove-by-id") nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -946,7 +946,7 @@ func TestRemoveMongoDB(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-both-params") nodeName := pmmapitests.TestString(t, "node-remove-both-params") nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) @@ -966,7 +966,7 @@ func TestRemoveMongoDB(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-wrong-type") nodeName := pmmapitests.TestString(t, "node-remove-wrong-type") nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) diff --git a/api-tests/management/mysql_test.go b/api-tests/management/mysql_test.go index a80f8dc3e3..2a38338b86 100644 --- a/api-tests/management/mysql_test.go +++ b/api-tests/management/mysql_test.go @@ -40,7 +40,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -116,7 +116,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -218,7 +218,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -279,7 +279,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-the-same-name") @@ -327,7 +327,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name") @@ -448,7 +448,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test")) @@ -480,7 +480,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) params := &mysql.AddMySQLParams{ @@ -498,7 +498,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -523,7 +523,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -549,7 +549,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -577,7 +577,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -601,7 +601,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -626,7 +626,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -701,7 +701,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -775,7 +775,7 @@ func TestAddMySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -881,7 +881,7 @@ func TestRemoveMySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-name") nodeName := pmmapitests.TestString(t, "node-remove-by-name") nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -912,7 +912,7 @@ func TestRemoveMySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-id") nodeName := pmmapitests.TestString(t, "node-remove-by-id") nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -943,7 +943,7 @@ func TestRemoveMySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-both-params") nodeName := pmmapitests.TestString(t, "node-remove-both-params") nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) @@ -963,7 +963,7 @@ func TestRemoveMySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-wrong-type") nodeName := pmmapitests.TestString(t, "node-remove-wrong-type") nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) diff --git a/api-tests/management/nodes_test.go b/api-tests/management/nodes_test.go index 7c417fea83..e9d53c799a 100644 --- a/api-tests/management/nodes_test.go +++ b/api-tests/management/nodes_test.go @@ -40,7 +40,7 @@ func TestNodeRegister(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) // Check Node is created assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{ @@ -58,14 +58,14 @@ func TestNodeRegister(t *testing.T) { }) t.Run("Reregister with same node name (no re-register - should fail)", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "all-fields") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), Address: "node-address-1", Region: "region-1", }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) body := node.RegisterNodeBody{ @@ -84,7 +84,7 @@ func TestNodeRegister(t *testing.T) { }) t.Run("Reregister with same node name (re-register)", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "all-fields") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), @@ -108,20 +108,21 @@ func TestNodeRegister(t *testing.T) { node, err := client.Default.Node.RegisterNode(¶ms) assert.NoError(t, err) - defer pmmapitests.RemoveNodes(t, node.Payload.GenericNode.NodeID) + defer pmmapitests.UnregisterNodes(t, node.Payload.GenericNode.NodeID) defer RemovePMMAgentWithSubAgents(t, node.Payload.PMMAgent.AgentID) + assertNodeExporterCreated(t, node.Payload.PMMAgent.AgentID) }) t.Run("Reregister with different node name (no re-register - should fail)", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "all-fields") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), Address: "node-address-3", Region: "region-3", }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) body := node.RegisterNodeBody{ @@ -140,16 +141,16 @@ func TestNodeRegister(t *testing.T) { }) t.Run("Reregister with different node name (re-register)", func(t *testing.T) { - nodeName := pmmapitests.TestString(t, "node-name-for-all-fields") + nodeName := pmmapitests.TestString(t, "all-fields") nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), Address: "node-address-4", Region: "region-4", }) - assert.NotEmpty(t, nodeID) assert.NotEmpty(t, pmmAgentID) + pmmapitests.UnregisterNodes(t, nodeID) body := node.RegisterNodeBody{ NodeName: nodeName + "_new", @@ -165,7 +166,7 @@ func TestNodeRegister(t *testing.T) { node, err := client.Default.Node.RegisterNode(¶ms) assert.NoError(t, err) - defer pmmapitests.RemoveNodes(t, node.Payload.GenericNode.NodeID) + defer pmmapitests.UnregisterNodes(t, node.Payload.GenericNode.NodeID) _, ok := assertNodeExporterCreated(t, node.Payload.PMMAgent.AgentID) if ok { defer RemovePMMAgentWithSubAgents(t, node.Payload.PMMAgent.AgentID) @@ -189,7 +190,7 @@ func TestNodeRegister(t *testing.T) { DisableCollectors: []string{"diskstats", "filesystem", "standard.process"}, } nodeID, pmmAgentID := RegisterGenericNode(t, body) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) // Check Node is created @@ -232,62 +233,6 @@ func TestNodeRegister(t *testing.T) { defer pmmapitests.RemoveAgents(t, nodeExporterAgentID) } }) - - t.Run("Re-register", func(t *testing.T) { - t.Skip("Re-register logic is not defined yet. https://jira.percona.com/browse/PMM-3717") - - nodeName := pmmapitests.TestString(t, "node-name") - nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ - NodeName: nodeName, - NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), - }) - defer pmmapitests.RemoveNodes(t, nodeID) - defer RemovePMMAgentWithSubAgents(t, pmmAgentID) - - // Check Node is created - assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{ - Generic: &nodes.GetNodeOKBodyGeneric{ - NodeID: nodeID, - NodeName: nodeName, - }, - }) - - // Re-register node - machineID := pmmapitests.TestString(t, "machine-id") - nodeModel := pmmapitests.TestString(t, "node-model") - newNodeID, newPMMAgentID := RegisterGenericNode(t, node.RegisterNodeBody{ - NodeName: nodeName, - NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), - MachineID: machineID, - NodeModel: nodeModel, - Az: "eu", - Region: "us-west", - Address: "10.10.10.10", - Distro: "Linux", - CustomLabels: map[string]string{"foo": "bar"}, - }) - if !assert.Equal(t, nodeID, newNodeID) { - defer pmmapitests.RemoveNodes(t, newNodeID) - } - if !assert.Equal(t, pmmAgentID, newPMMAgentID) { - defer pmmapitests.RemoveAgents(t, newPMMAgentID) - } - - // Check Node fields is updated - assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{ - Generic: &nodes.GetNodeOKBodyGeneric{ - NodeID: nodeID, - NodeName: nodeName, - MachineID: machineID, - NodeModel: nodeModel, - Az: "eu", - Region: "us-west", - Address: "10.10.10.10", - Distro: "Linux", - CustomLabels: map[string]string{"foo": "bar"}, - }, - }) - }) }) t.Run("Container Node", func(t *testing.T) { @@ -297,7 +242,7 @@ func TestNodeRegister(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) // Check Node is created @@ -335,7 +280,7 @@ func TestNodeRegister(t *testing.T) { CustomLabels: map[string]string{"foo": "bar"}, } nodeID, pmmAgentID := registerContainerNode(t, body) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) // Check Node is created @@ -362,63 +307,6 @@ func TestNodeRegister(t *testing.T) { defer pmmapitests.RemoveAgents(t, nodeExporterAgentID) } }) - - t.Run("Re-register", func(t *testing.T) { - t.Skip("Re-register logic is not defined yet. https://jira.percona.com/browse/PMM-3717") - - nodeName := pmmapitests.TestString(t, "node-name") - nodeID, pmmAgentID := registerContainerNode(t, node.RegisterNodeBody{ - NodeName: nodeName, - NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE), - }) - defer pmmapitests.RemoveNodes(t, nodeID) - defer RemovePMMAgentWithSubAgents(t, pmmAgentID) - - // Check Node is created - assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{ - Generic: &nodes.GetNodeOKBodyGeneric{ - NodeID: nodeID, - NodeName: nodeName, - }, - }) - - // Re-register node - nodeModel := pmmapitests.TestString(t, "node-model") - containerID := pmmapitests.TestString(t, "container-id") - containerName := pmmapitests.TestString(t, "container-name") - newNodeID, newPMMAgentID := registerContainerNode(t, node.RegisterNodeBody{ - NodeName: nodeName, - NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE), - ContainerID: containerID, - ContainerName: containerName, - NodeModel: nodeModel, - Az: "eu", - Region: "us-west", - Address: "10.10.10.10", - CustomLabels: map[string]string{"foo": "bar"}, - }) - if !assert.Equal(t, nodeID, newNodeID) { - defer pmmapitests.RemoveNodes(t, newNodeID) - } - if !assert.Equal(t, pmmAgentID, newPMMAgentID) { - defer pmmapitests.RemoveAgents(t, newPMMAgentID) - } - - // Check Node fields is updated - assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{ - Container: &nodes.GetNodeOKBodyContainer{ - NodeID: nodeID, - NodeName: nodeName, - ContainerID: containerID, - ContainerName: containerName, - NodeModel: nodeModel, - Az: "eu", - Region: "us-west", - Address: "10.10.10.10", - CustomLabels: map[string]string{"foo": "bar"}, - }, - }) - }) }) t.Run("Empty node name", func(t *testing.T) { diff --git a/api-tests/management/postgresql_test.go b/api-tests/management/postgresql_test.go index a6ac1b9494..ff46701eee 100644 --- a/api-tests/management/postgresql_test.go +++ b/api-tests/management/postgresql_test.go @@ -42,7 +42,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -122,7 +122,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -227,7 +227,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -284,7 +284,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-the-same-name") @@ -333,7 +333,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name") @@ -457,7 +457,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test")) @@ -489,7 +489,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) params := &postgresql.AddPostgreSQLParams{ @@ -507,7 +507,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -531,7 +531,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -556,7 +556,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -580,7 +580,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -608,7 +608,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -684,7 +684,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -759,7 +759,7 @@ func TestAddPostgreSQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -866,7 +866,7 @@ func TestRemovePostgreSQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-name") nodeName := pmmapitests.TestString(t, "node-remove-by-name") nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -897,7 +897,7 @@ func TestRemovePostgreSQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-id") nodeName := pmmapitests.TestString(t, "node-remove-by-id") nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, true) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -928,7 +928,7 @@ func TestRemovePostgreSQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-both-params") nodeName := pmmapitests.TestString(t, "node-remove-both-params") nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) @@ -948,7 +948,7 @@ func TestRemovePostgreSQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-wrong-type") nodeName := pmmapitests.TestString(t, "node-remove-wrong-type") nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, false) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) diff --git a/api-tests/management/proxysql_test.go b/api-tests/management/proxysql_test.go index e7bc9cd594..99437ba7e0 100644 --- a/api-tests/management/proxysql_test.go +++ b/api-tests/management/proxysql_test.go @@ -40,7 +40,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-basic-name") @@ -115,7 +115,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -191,7 +191,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-all-fields-name") @@ -252,7 +252,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-for-the-same-name") @@ -300,7 +300,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name") @@ -420,7 +420,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test")) @@ -452,7 +452,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) params := &proxysql.AddProxySQLParams{ @@ -470,7 +470,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -494,7 +494,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -519,7 +519,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -547,7 +547,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -571,7 +571,7 @@ func TestAddProxySQL(t *testing.T) { NodeName: nodeName, NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE), }) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) serviceName := pmmapitests.TestString(t, "service-name") @@ -625,7 +625,7 @@ func TestRemoveProxySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-name") nodeName := pmmapitests.TestString(t, "node-remove-by-name") nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -656,7 +656,7 @@ func TestRemoveProxySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-by-id") nodeName := pmmapitests.TestString(t, "node-remove-by-id") nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{ @@ -687,7 +687,7 @@ func TestRemoveProxySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-both-params") nodeName := pmmapitests.TestString(t, "node-remove-both-params") nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) @@ -707,7 +707,7 @@ func TestRemoveProxySQL(t *testing.T) { serviceName := pmmapitests.TestString(t, "service-remove-wrong-type") nodeName := pmmapitests.TestString(t, "node-remove-wrong-type") nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName) - defer pmmapitests.RemoveNodes(t, nodeID) + defer pmmapitests.UnregisterNodes(t, nodeID) defer pmmapitests.RemoveServices(t, serviceID) defer RemovePMMAgentWithSubAgents(t, pmmAgentID) diff --git a/api-tests/server/auth_test.go b/api-tests/server/auth_test.go index 979b89062a..a7c17e7c3c 100644 --- a/api-tests/server/auth_test.go +++ b/api-tests/server/auth_test.go @@ -38,6 +38,11 @@ import ( "github.com/percona/pmm/api/serverpb/json/client/server" ) +const ( + pmmServiceTokenName = "pmm-agent-service-token" //nolint:gosec + pmmServiceAccountName = "pmm-agent-service-account" //nolint:gosec +) + func TestAuth(t *testing.T) { t.Run("AuthErrors", func(t *testing.T) { for user, code := range map[*url.Userinfo]int{ @@ -240,7 +245,20 @@ func TestSwagger(t *testing.T) { } } -func TestPermissions(t *testing.T) { +func doRequest(tb testing.TB, client *http.Client, req *http.Request) (*http.Response, []byte) { + tb.Helper() + resp, err := client.Do(req) + require.NoError(tb, err) + + defer resp.Body.Close() //nolint:errcheck + + b, err := io.ReadAll(resp.Body) + require.NoError(tb, err) + + return resp, b +} + +func TestBasicAuthPermissions(t *testing.T) { ts := strconv.FormatInt(time.Now().Unix(), 10) none := "none-" + ts viewer := "viewer-" + ts @@ -259,13 +277,12 @@ func TestPermissions(t *testing.T) { adminID := createUserWithRole(t, admin, "Admin") defer deleteUser(t, adminID) - viewerAPIKeyID, viewerAPIKey := createAPIKeyWithRole(t, "api-"+viewer, "Viewer") + const apiPrefix = "api" + viewerAPIKeyID, viewerAPIKey := createAPIKeyWithRole(t, fmt.Sprintf("%s-%s", apiPrefix, viewer), "Viewer") defer deleteAPIKey(t, viewerAPIKeyID) - - editorAPIKeyID, editorAPIKey := createAPIKeyWithRole(t, "api-"+editor, "Editor") + editorAPIKeyID, editorAPIKey := createAPIKeyWithRole(t, fmt.Sprintf("%s-%s", apiPrefix, editor), "Editor") defer deleteAPIKey(t, editorAPIKeyID) - - adminAPIKeyID, adminAPIKey := createAPIKeyWithRole(t, "api-"+admin, "Admin") + adminAPIKeyID, adminAPIKey := createAPIKeyWithRole(t, fmt.Sprintf("%s-%s", apiPrefix, admin), "Admin") defer deleteAPIKey(t, adminAPIKeyID) type userCase struct { @@ -363,19 +380,6 @@ func TestPermissions(t *testing.T) { } } -func doRequest(tb testing.TB, client *http.Client, req *http.Request) (*http.Response, []byte) { - tb.Helper() - resp, err := client.Do(req) - require.NoError(tb, err) - - defer resp.Body.Close() //nolint:gosec,errcheck,nolintlint - - b, err := io.ReadAll(resp.Body) - require.NoError(tb, err) - - return resp, b -} - func createUserWithRole(t *testing.T, login, role string) int { t.Helper() userID := createUser(t, login) @@ -452,20 +456,99 @@ func setRole(t *testing.T, userID int, role string) { require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to set role for user, response: %s", b) } -func deleteAPIKey(t *testing.T, apiKeyID int) { - t.Helper() - // https://grafana.com/docs/grafana/latest/http_api/auth/#delete-api-key - u, err := url.Parse(pmmapitests.BaseURL.String()) - require.NoError(t, err) - u.Path = "/graph/api/auth/keys/" + strconv.Itoa(apiKeyID) +func TestServiceAccountPermissions(t *testing.T) { + // service account role options: viewer, editor, admin + // service token role options: editor, admin + // basic auth format is skipped, endpoint /auth/serviceaccount (to get info about currently used token in request) requires Bearer authorization + // service_token:token format could be used in pmm-agent and pmm-admin (its transformed into Bearer authorization) + nodeName := "test-node" + + viewerNodeName := fmt.Sprintf("%s-viewer", nodeName) + viewerAccountID := createServiceAccountWithRole(t, "Viewer", viewerNodeName) + viewerTokenID, viewerToken := createServiceToken(t, viewerAccountID, viewerNodeName) + defer deleteServiceAccount(t, viewerAccountID) + defer deleteServiceToken(t, viewerAccountID, viewerTokenID) + + editorNodeName := fmt.Sprintf("%s-editor", nodeName) + editorAccountID := createServiceAccountWithRole(t, "Editor", editorNodeName) + editorTokenID, editorToken := createServiceToken(t, editorAccountID, editorNodeName) + defer deleteServiceAccount(t, editorAccountID) + defer deleteServiceToken(t, editorAccountID, editorTokenID) + + adminNodeName := fmt.Sprintf("%s-admin", nodeName) + adminAccountID := createServiceAccountWithRole(t, "Admin", adminNodeName) + adminTokenID, adminToken := createServiceToken(t, adminAccountID, adminNodeName) + defer deleteServiceAccount(t, adminAccountID) + defer deleteServiceToken(t, adminAccountID, adminTokenID) - req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil) - require.NoError(t, err) + type userCase struct { + userType string + serviceToken string + statusCode int + } - resp, b := doRequest(t, http.DefaultClient, req) - defer resp.Body.Close() //nolint:gosec,errcheck,nolintlint + tests := []struct { + name string + url string + method string + userCase []userCase + }{ + {name: "settings", url: "/v1/Settings/Get", method: "POST", userCase: []userCase{ + {userType: "default", statusCode: 401}, + {userType: "viewer", serviceToken: viewerToken, statusCode: 401}, + {userType: "editor", serviceToken: editorToken, statusCode: 401}, + {userType: "admin", serviceToken: adminToken, statusCode: 200}, + }}, + {name: "platform-connect", url: "/v1/Platform/Connect", method: "POST", userCase: []userCase{ + {userType: "default", statusCode: 401}, + {userType: "viewer", serviceToken: viewerToken, statusCode: 401}, + {userType: "editor", serviceToken: editorToken, statusCode: 401}, + {userType: "admin", serviceToken: adminToken, statusCode: 400}, // We send bad request, but have access to endpoint + }}, + } - require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete API Key, status code: %d, response: %s", resp.StatusCode, b) + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + for _, user := range test.userCase { + user := user + t.Run(fmt.Sprintf("Service Token auth %s", user.userType), func(t *testing.T) { + // make a BaseURL without authentication + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.User = nil + u.Path = test.url + + req, err := http.NewRequestWithContext(pmmapitests.Context, test.method, u.String(), nil) + require.NoError(t, err) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.serviceToken)) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + + assert.Equal(t, user.statusCode, resp.StatusCode) + }) + + t.Run(fmt.Sprintf("Basic auth with Service Token %s", user.userType), func(t *testing.T) { + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.User = url.UserPassword("service_token", user.serviceToken) + u.Path = test.url + + req, err := http.NewRequestWithContext(pmmapitests.Context, test.method, u.String(), nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + + assert.Equal(t, user.statusCode, resp.StatusCode) + }) + } + }) + } } func createAPIKeyWithRole(t *testing.T, name, role string) (int, string) { @@ -515,3 +598,125 @@ func createAPIKeyWithRole(t *testing.T, name, role string) (int, string) { return apiKeyID, apiKey } + +func deleteAPIKey(t *testing.T, apiKeyID int) { + t.Helper() + // https://grafana.com/docs/grafana/latest/http_api/auth/#delete-api-key + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.Path = "/graph/api/auth/keys/" + strconv.Itoa(apiKeyID) + + req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil) + require.NoError(t, err) + + resp, b := doRequest(t, http.DefaultClient, req) + defer resp.Body.Close() //nolint:gosec,errcheck,nolintlint + + require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete API Key, status code: %d, response: %s", resp.StatusCode, b) +} + +func createServiceAccountWithRole(t *testing.T, role, nodeName string) int { + t.Helper() + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.Path = "/graph/api/serviceaccounts" + + name := fmt.Sprintf("%s-%s", pmmServiceAccountName, nodeName) + data, err := json.Marshal(map[string]string{ + "name": name, + "role": role, + }) + require.NoError(t, err) + + req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodPost, u.String(), bytes.NewReader(data)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + resp, b := doRequest(t, http.DefaultClient, req) + defer resp.Body.Close() //nolint:errcheck + + require.Equalf(t, http.StatusCreated, resp.StatusCode, "failed to create Service account, status code: %d, response: %s", resp.StatusCode, b) + + var m map[string]interface{} + err = json.Unmarshal(b, &m) + require.NoError(t, err) + + serviceAccountID := int(m["id"].(float64)) + u.Path = fmt.Sprintf("/graph/api/serviceaccounts/%d", serviceAccountID) + data, err = json.Marshal(map[string]string{ + "orgId": "1", + }) + require.NoError(t, err) + + req, err = http.NewRequestWithContext(pmmapitests.Context, http.MethodPatch, u.String(), bytes.NewReader(data)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + resp1, b := doRequest(t, http.DefaultClient, req) + defer resp1.Body.Close() //nolint:errcheck + + require.Equalf(t, http.StatusCreated, resp.StatusCode, "failed to set orgId=1 to Service account, status code: %d, response: %s", resp.StatusCode, b) + + return serviceAccountID +} + +func deleteServiceAccount(t *testing.T, serviceAccountID int) { + t.Helper() + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.Path = fmt.Sprintf("/graph/api/serviceaccounts/%d", serviceAccountID) + + req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil) + require.NoError(t, err) + + resp, b := doRequest(t, http.DefaultClient, req) + defer resp.Body.Close() //nolint:gosec,errcheck,nolintlint + + require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete service account, status code: %d, response: %s", resp.StatusCode, b) +} + +func createServiceToken(t *testing.T, serviceAccountID int, nodeName string) (int, string) { + t.Helper() + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.Path = fmt.Sprintf("/graph/api/serviceaccounts/%d/tokens", serviceAccountID) + + name := fmt.Sprintf("%s-%s", pmmServiceTokenName, nodeName) + data, err := json.Marshal(map[string]string{ + "name": name, + }) + require.NoError(t, err) + + req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodPost, u.String(), bytes.NewReader(data)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + resp, b := doRequest(t, http.DefaultClient, req) + defer resp.Body.Close() //nolint:gosec,errcheck,nolintlint + + require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to create Service account, status code: %d, response: %s", resp.StatusCode, b) + + var m map[string]interface{} + err = json.Unmarshal(b, &m) + require.NoError(t, err) + + return int(m["id"].(float64)), m["key"].(string) +} + +func deleteServiceToken(t *testing.T, serviceAccountID, serviceTokenID int) { + t.Helper() + u, err := url.Parse(pmmapitests.BaseURL.String()) + require.NoError(t, err) + u.Path = fmt.Sprintf("/graph/api/serviceaccounts/%d/tokens/%d", serviceAccountID, serviceTokenID) + + req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil) + require.NoError(t, err) + + resp, b := doRequest(t, http.DefaultClient, req) + defer resp.Body.Close() //nolint:errcheck + + require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete service token, status code: %d, response: %s", resp.StatusCode, b) +} diff --git a/api/managementpb/json/client/node/node_client.go b/api/managementpb/json/client/node/node_client.go index 49d79d9e66..9a1855222a 100644 --- a/api/managementpb/json/client/node/node_client.go +++ b/api/managementpb/json/client/node/node_client.go @@ -30,6 +30,8 @@ type ClientOption func(*runtime.ClientOperation) type ClientService interface { RegisterNode(params *RegisterNodeParams, opts ...ClientOption) (*RegisterNodeOK, error) + UnregisterNode(params *UnregisterNodeParams, opts ...ClientOption) (*UnregisterNodeOK, error) + SetTransport(transport runtime.ClientTransport) } @@ -72,6 +74,45 @@ func (a *Client) RegisterNode(params *RegisterNodeParams, opts ...ClientOption) return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) } +/* +UnregisterNode unregisters node + +Unregister a Node and pmm-agent +*/ +func (a *Client) UnregisterNode(params *UnregisterNodeParams, opts ...ClientOption) (*UnregisterNodeOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUnregisterNodeParams() + } + op := &runtime.ClientOperation{ + ID: "UnregisterNode", + Method: "POST", + PathPattern: "/v1/management/Node/Unregister", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &UnregisterNodeReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UnregisterNodeOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*UnregisterNodeDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/api/managementpb/json/client/node/unregister_node_parameters.go b/api/managementpb/json/client/node/unregister_node_parameters.go new file mode 100644 index 0000000000..579b04d3a9 --- /dev/null +++ b/api/managementpb/json/client/node/unregister_node_parameters.go @@ -0,0 +1,144 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package node + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewUnregisterNodeParams creates a new UnregisterNodeParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUnregisterNodeParams() *UnregisterNodeParams { + return &UnregisterNodeParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUnregisterNodeParamsWithTimeout creates a new UnregisterNodeParams object +// with the ability to set a timeout on a request. +func NewUnregisterNodeParamsWithTimeout(timeout time.Duration) *UnregisterNodeParams { + return &UnregisterNodeParams{ + timeout: timeout, + } +} + +// NewUnregisterNodeParamsWithContext creates a new UnregisterNodeParams object +// with the ability to set a context for a request. +func NewUnregisterNodeParamsWithContext(ctx context.Context) *UnregisterNodeParams { + return &UnregisterNodeParams{ + Context: ctx, + } +} + +// NewUnregisterNodeParamsWithHTTPClient creates a new UnregisterNodeParams object +// with the ability to set a custom HTTPClient for a request. +func NewUnregisterNodeParamsWithHTTPClient(client *http.Client) *UnregisterNodeParams { + return &UnregisterNodeParams{ + HTTPClient: client, + } +} + +/* +UnregisterNodeParams contains all the parameters to send to the API endpoint + + for the unregister node operation. + + Typically these are written to a http.Request. +*/ +type UnregisterNodeParams struct { + // Body. + Body UnregisterNodeBody + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the unregister node params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UnregisterNodeParams) WithDefaults() *UnregisterNodeParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the unregister node params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UnregisterNodeParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the unregister node params +func (o *UnregisterNodeParams) WithTimeout(timeout time.Duration) *UnregisterNodeParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the unregister node params +func (o *UnregisterNodeParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the unregister node params +func (o *UnregisterNodeParams) WithContext(ctx context.Context) *UnregisterNodeParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the unregister node params +func (o *UnregisterNodeParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the unregister node params +func (o *UnregisterNodeParams) WithHTTPClient(client *http.Client) *UnregisterNodeParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the unregister node params +func (o *UnregisterNodeParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the unregister node params +func (o *UnregisterNodeParams) WithBody(body UnregisterNodeBody) *UnregisterNodeParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the unregister node params +func (o *UnregisterNodeParams) SetBody(body UnregisterNodeBody) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *UnregisterNodeParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/api/managementpb/json/client/node/unregister_node_responses.go b/api/managementpb/json/client/node/unregister_node_responses.go new file mode 100644 index 0000000000..f2bd01755a --- /dev/null +++ b/api/managementpb/json/client/node/unregister_node_responses.go @@ -0,0 +1,337 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package node + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "fmt" + "io" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// UnregisterNodeReader is a Reader for the UnregisterNode structure. +type UnregisterNodeReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UnregisterNodeReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUnregisterNodeOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewUnregisterNodeDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewUnregisterNodeOK creates a UnregisterNodeOK with default headers values +func NewUnregisterNodeOK() *UnregisterNodeOK { + return &UnregisterNodeOK{} +} + +/* +UnregisterNodeOK describes a response with status code 200, with default header values. + +A successful response. +*/ +type UnregisterNodeOK struct { + Payload *UnregisterNodeOKBody +} + +func (o *UnregisterNodeOK) Error() string { + return fmt.Sprintf("[POST /v1/management/Node/Unregister][%d] unregisterNodeOk %+v", 200, o.Payload) +} + +func (o *UnregisterNodeOK) GetPayload() *UnregisterNodeOKBody { + return o.Payload +} + +func (o *UnregisterNodeOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + o.Payload = new(UnregisterNodeOKBody) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUnregisterNodeDefault creates a UnregisterNodeDefault with default headers values +func NewUnregisterNodeDefault(code int) *UnregisterNodeDefault { + return &UnregisterNodeDefault{ + _statusCode: code, + } +} + +/* +UnregisterNodeDefault describes a response with status code -1, with default header values. + +An unexpected error response. +*/ +type UnregisterNodeDefault struct { + _statusCode int + + Payload *UnregisterNodeDefaultBody +} + +// Code gets the status code for the unregister node default response +func (o *UnregisterNodeDefault) Code() int { + return o._statusCode +} + +func (o *UnregisterNodeDefault) Error() string { + return fmt.Sprintf("[POST /v1/management/Node/Unregister][%d] UnregisterNode default %+v", o._statusCode, o.Payload) +} + +func (o *UnregisterNodeDefault) GetPayload() *UnregisterNodeDefaultBody { + return o.Payload +} + +func (o *UnregisterNodeDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + o.Payload = new(UnregisterNodeDefaultBody) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +/* +UnregisterNodeBody unregister node body +swagger:model UnregisterNodeBody +*/ +type UnregisterNodeBody struct { + // Node_id to be unregistered. + NodeID string `json:"node_id,omitempty"` + + // Force delete node, related service account, even if it has more service tokens attached. + Force bool `json:"force,omitempty"` +} + +// Validate validates this unregister node body +func (o *UnregisterNodeBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this unregister node body based on context it is used +func (o *UnregisterNodeBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *UnregisterNodeBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *UnregisterNodeBody) UnmarshalBinary(b []byte) error { + var res UnregisterNodeBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} + +/* +UnregisterNodeDefaultBody unregister node default body +swagger:model UnregisterNodeDefaultBody +*/ +type UnregisterNodeDefaultBody struct { + // code + Code int32 `json:"code,omitempty"` + + // message + Message string `json:"message,omitempty"` + + // details + Details []*UnregisterNodeDefaultBodyDetailsItems0 `json:"details"` +} + +// Validate validates this unregister node default body +func (o *UnregisterNodeDefaultBody) Validate(formats strfmt.Registry) error { + var res []error + + if err := o.validateDetails(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (o *UnregisterNodeDefaultBody) validateDetails(formats strfmt.Registry) error { + if swag.IsZero(o.Details) { // not required + return nil + } + + for i := 0; i < len(o.Details); i++ { + if swag.IsZero(o.Details[i]) { // not required + continue + } + + if o.Details[i] != nil { + if err := o.Details[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("UnregisterNode default" + "." + "details" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("UnregisterNode default" + "." + "details" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this unregister node default body based on the context it is used +func (o *UnregisterNodeDefaultBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := o.contextValidateDetails(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (o *UnregisterNodeDefaultBody) contextValidateDetails(ctx context.Context, formats strfmt.Registry) error { + for i := 0; i < len(o.Details); i++ { + if o.Details[i] != nil { + if err := o.Details[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("UnregisterNode default" + "." + "details" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("UnregisterNode default" + "." + "details" + "." + strconv.Itoa(i)) + } + return err + } + } + } + + return nil +} + +// MarshalBinary interface implementation +func (o *UnregisterNodeDefaultBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *UnregisterNodeDefaultBody) UnmarshalBinary(b []byte) error { + var res UnregisterNodeDefaultBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} + +/* +UnregisterNodeDefaultBodyDetailsItems0 unregister node default body details items0 +swagger:model UnregisterNodeDefaultBodyDetailsItems0 +*/ +type UnregisterNodeDefaultBodyDetailsItems0 struct { + // at type + AtType string `json:"@type,omitempty"` +} + +// Validate validates this unregister node default body details items0 +func (o *UnregisterNodeDefaultBodyDetailsItems0) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this unregister node default body details items0 based on context it is used +func (o *UnregisterNodeDefaultBodyDetailsItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *UnregisterNodeDefaultBodyDetailsItems0) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *UnregisterNodeDefaultBodyDetailsItems0) UnmarshalBinary(b []byte) error { + var res UnregisterNodeDefaultBodyDetailsItems0 + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} + +/* +UnregisterNodeOKBody unregister node OK body +swagger:model UnregisterNodeOKBody +*/ +type UnregisterNodeOKBody struct { + // Warning message if there are more service tokens attached to service account. + Warning string `json:"warning,omitempty"` +} + +// Validate validates this unregister node OK body +func (o *UnregisterNodeOKBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this unregister node OK body based on context it is used +func (o *UnregisterNodeOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *UnregisterNodeOKBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *UnregisterNodeOKBody) UnmarshalBinary(b []byte) error { + var res UnregisterNodeOKBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} diff --git a/api/managementpb/json/managementpb.json b/api/managementpb/json/managementpb.json index fe14bc61d0..a0fc1f8a8e 100644 --- a/api/managementpb/json/managementpb.json +++ b/api/managementpb/json/managementpb.json @@ -3967,6 +3967,84 @@ } } }, + "/v1/management/Node/Unregister": { + "post": { + "description": "Unregister a Node and pmm-agent", + "tags": [ + "Node" + ], + "summary": "Unregister Node", + "operationId": "UnregisterNode", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "force": { + "description": "Force delete node, related service account, even if it has more service tokens attached.", + "type": "boolean", + "x-order": 1 + }, + "node_id": { + "description": "Node_id to be unregistered.", + "type": "string", + "x-order": 0 + } + } + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": { + "warning": { + "description": "Warning message if there are more service tokens attached to service account.", + "type": "string", + "x-order": 0 + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "x-order": 0 + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "x-order": 0 + } + }, + "additionalProperties": false + }, + "x-order": 2 + }, + "message": { + "type": "string", + "x-order": 1 + } + } + } + } + } + } + }, "/v1/management/PostgreSQL/Add": { "post": { "description": "Adds PostgreSQL Service and starts postgres exporter. It automatically adds a service to inventory, which is running on provided \"node_id\", then adds \"postgres_exporter\" with provided \"pmm_agent_id\" and other parameters.", diff --git a/api/managementpb/node.pb.go b/api/managementpb/node.pb.go index f49a1fb596..b8328a867f 100644 --- a/api/managementpb/node.pb.go +++ b/api/managementpb/node.pb.go @@ -292,6 +292,111 @@ func (x *RegisterNodeResponse) GetWarning() string { return "" } +type UnregisterNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Node_id to be unregistered. + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + // Force delete node, related service account, even if it has more service tokens attached. + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` +} + +func (x *UnregisterNodeRequest) Reset() { + *x = UnregisterNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_managementpb_node_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnregisterNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterNodeRequest) ProtoMessage() {} + +func (x *UnregisterNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_managementpb_node_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterNodeRequest.ProtoReflect.Descriptor instead. +func (*UnregisterNodeRequest) Descriptor() ([]byte, []int) { + return file_managementpb_node_proto_rawDescGZIP(), []int{2} +} + +func (x *UnregisterNodeRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *UnregisterNodeRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +type UnregisterNodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Warning message if there are more service tokens attached to service account. + Warning string `protobuf:"bytes,1,opt,name=warning,proto3" json:"warning,omitempty"` +} + +func (x *UnregisterNodeResponse) Reset() { + *x = UnregisterNodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_managementpb_node_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnregisterNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterNodeResponse) ProtoMessage() {} + +func (x *UnregisterNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_managementpb_node_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterNodeResponse.ProtoReflect.Descriptor instead. +func (*UnregisterNodeResponse) Descriptor() ([]byte, []int) { + return file_managementpb_node_proto_rawDescGZIP(), []int{3} +} + +func (x *UnregisterNodeResponse) GetWarning() string { + if x != nil { + return x.Warning + } + return "" +} + var File_managementpb_node_proto protoreflect.FileDescriptor var file_managementpb_node_proto_rawDesc = []byte{ @@ -368,28 +473,48 @@ var file_managementpb_node_proto_rawDesc = []byte{ 0x70, 0x6d, 0x6d, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x32, 0xba, 0x01, 0x0a, 0x04, 0x4e, 0x6f, 0x64, - 0x65, 0x12, 0xb1, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, - 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5e, 0x92, 0x41, 0x34, 0x12, 0x0d, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x20, 0x4e, 0x6f, 0x64, 0x65, 0x1a, 0x23, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x73, 0x20, 0x61, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x4e, 0x6f, 0x64, 0x65, 0x20, - 0x61, 0x6e, 0x64, 0x20, 0x70, 0x6d, 0x6d, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x4e, 0x6f, 0x64, 0x65, 0x2f, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x42, 0x8c, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x09, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x70, 0x65, 0x72, 0x63, 0x6f, 0x6e, 0x61, 0x2f, 0x70, 0x6d, 0x6d, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x70, 0x62, 0xa2, 0x02, - 0x03, 0x4d, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0xca, 0x02, 0x0a, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0xe2, 0x02, - 0x16, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5c, 0x47, 0x50, 0x42, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x46, 0x0a, 0x15, 0x55, 0x6e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x22, 0x32, 0x0a, 0x16, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, + 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x77, 0x61, 0x72, + 0x6e, 0x69, 0x6e, 0x67, 0x32, 0xf4, 0x02, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0xb1, 0x01, + 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x5e, 0x92, 0x41, 0x34, 0x12, 0x0d, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x20, 0x4e, 0x6f, 0x64, 0x65, 0x1a, 0x23, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x73, + 0x20, 0x61, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x4e, 0x6f, 0x64, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, + 0x70, 0x6d, 0x6d, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, + 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2f, 0x4e, 0x6f, 0x64, 0x65, 0x2f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x12, 0xb7, 0x01, 0x0a, 0x0e, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5e, 0x92, 0x41, 0x32, + 0x12, 0x0f, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x20, 0x4e, 0x6f, 0x64, + 0x65, 0x1a, 0x1f, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x20, 0x61, 0x20, + 0x4e, 0x6f, 0x64, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x6d, 0x6d, 0x2d, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x01, 0x2a, 0x22, 0x1e, 0x2f, 0x76, 0x31, + 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x4e, 0x6f, 0x64, 0x65, + 0x2f, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x42, 0x8c, 0x01, 0x0a, 0x0e, + 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x09, + 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x27, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x65, 0x72, 0x63, 0x6f, 0x6e, 0x61, 0x2f, + 0x70, 0x6d, 0x6d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x70, 0x62, 0xa2, 0x02, 0x03, 0x4d, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0xca, 0x02, 0x0a, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0xe2, 0x02, 0x16, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -405,30 +530,34 @@ func file_managementpb_node_proto_rawDescGZIP() []byte { } var ( - file_managementpb_node_proto_msgTypes = make([]protoimpl.MessageInfo, 3) + file_managementpb_node_proto_msgTypes = make([]protoimpl.MessageInfo, 5) file_managementpb_node_proto_goTypes = []interface{}{ (*RegisterNodeRequest)(nil), // 0: management.RegisterNodeRequest (*RegisterNodeResponse)(nil), // 1: management.RegisterNodeResponse - nil, // 2: management.RegisterNodeRequest.CustomLabelsEntry - (inventorypb.NodeType)(0), // 3: inventory.NodeType - (MetricsMode)(0), // 4: management.MetricsMode - (*inventorypb.GenericNode)(nil), // 5: inventory.GenericNode - (*inventorypb.ContainerNode)(nil), // 6: inventory.ContainerNode - (*inventorypb.PMMAgent)(nil), // 7: inventory.PMMAgent + (*UnregisterNodeRequest)(nil), // 2: management.UnregisterNodeRequest + (*UnregisterNodeResponse)(nil), // 3: management.UnregisterNodeResponse + nil, // 4: management.RegisterNodeRequest.CustomLabelsEntry + (inventorypb.NodeType)(0), // 5: inventory.NodeType + (MetricsMode)(0), // 6: management.MetricsMode + (*inventorypb.GenericNode)(nil), // 7: inventory.GenericNode + (*inventorypb.ContainerNode)(nil), // 8: inventory.ContainerNode + (*inventorypb.PMMAgent)(nil), // 9: inventory.PMMAgent } ) var file_managementpb_node_proto_depIdxs = []int32{ - 3, // 0: management.RegisterNodeRequest.node_type:type_name -> inventory.NodeType - 2, // 1: management.RegisterNodeRequest.custom_labels:type_name -> management.RegisterNodeRequest.CustomLabelsEntry - 4, // 2: management.RegisterNodeRequest.metrics_mode:type_name -> management.MetricsMode - 5, // 3: management.RegisterNodeResponse.generic_node:type_name -> inventory.GenericNode - 6, // 4: management.RegisterNodeResponse.container_node:type_name -> inventory.ContainerNode - 7, // 5: management.RegisterNodeResponse.pmm_agent:type_name -> inventory.PMMAgent + 5, // 0: management.RegisterNodeRequest.node_type:type_name -> inventory.NodeType + 4, // 1: management.RegisterNodeRequest.custom_labels:type_name -> management.RegisterNodeRequest.CustomLabelsEntry + 6, // 2: management.RegisterNodeRequest.metrics_mode:type_name -> management.MetricsMode + 7, // 3: management.RegisterNodeResponse.generic_node:type_name -> inventory.GenericNode + 8, // 4: management.RegisterNodeResponse.container_node:type_name -> inventory.ContainerNode + 9, // 5: management.RegisterNodeResponse.pmm_agent:type_name -> inventory.PMMAgent 0, // 6: management.Node.RegisterNode:input_type -> management.RegisterNodeRequest - 1, // 7: management.Node.RegisterNode:output_type -> management.RegisterNodeResponse - 7, // [7:8] is the sub-list for method output_type - 6, // [6:7] is the sub-list for method input_type + 2, // 7: management.Node.UnregisterNode:input_type -> management.UnregisterNodeRequest + 1, // 8: management.Node.RegisterNode:output_type -> management.RegisterNodeResponse + 3, // 9: management.Node.UnregisterNode:output_type -> management.UnregisterNodeResponse + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name @@ -465,6 +594,30 @@ func file_managementpb_node_proto_init() { return nil } } + file_managementpb_node_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnregisterNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_managementpb_node_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnregisterNodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -472,7 +625,7 @@ func file_managementpb_node_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_managementpb_node_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/api/managementpb/node.pb.gw.go b/api/managementpb/node.pb.gw.go index e6c176b244..450d756630 100644 --- a/api/managementpb/node.pb.gw.go +++ b/api/managementpb/node.pb.gw.go @@ -57,6 +57,30 @@ func local_request_Node_RegisterNode_0(ctx context.Context, marshaler runtime.Ma return msg, metadata, err } +func request_Node_UnregisterNode_0(ctx context.Context, marshaler runtime.Marshaler, client NodeClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnregisterNodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.UnregisterNode(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_Node_UnregisterNode_0(ctx context.Context, marshaler runtime.Marshaler, server NodeServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnregisterNodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UnregisterNode(ctx, &protoReq) + return msg, metadata, err +} + // RegisterNodeHandlerServer registers the http handlers for service Node to "mux". // UnaryRPC :call NodeServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -86,6 +110,30 @@ func RegisterNodeHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve forward_Node_RegisterNode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle("POST", pattern_Node_UnregisterNode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/management.Node/UnregisterNode", runtime.WithHTTPPathPattern("/v1/management/Node/Unregister")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Node_UnregisterNode_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Node_UnregisterNode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil } @@ -147,9 +195,38 @@ func RegisterNodeHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien forward_Node_RegisterNode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle("POST", pattern_Node_UnregisterNode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/management.Node/UnregisterNode", runtime.WithHTTPPathPattern("/v1/management/Node/Unregister")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Node_UnregisterNode_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Node_UnregisterNode_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil } -var pattern_Node_RegisterNode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "management", "Node", "Register"}, "")) +var ( + pattern_Node_RegisterNode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "management", "Node", "Register"}, "")) + + pattern_Node_UnregisterNode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "management", "Node", "Unregister"}, "")) +) -var forward_Node_RegisterNode_0 = runtime.ForwardResponseMessage +var ( + forward_Node_RegisterNode_0 = runtime.ForwardResponseMessage + + forward_Node_UnregisterNode_0 = runtime.ForwardResponseMessage +) diff --git a/api/managementpb/node.pb.validate.go b/api/managementpb/node.pb.validate.go index 0a0b03de4f..4adf81e156 100644 --- a/api/managementpb/node.pb.validate.go +++ b/api/managementpb/node.pb.validate.go @@ -372,3 +372,213 @@ var _ interface { Cause() error ErrorName() string } = RegisterNodeResponseValidationError{} + +// Validate checks the field values on UnregisterNodeRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *UnregisterNodeRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UnregisterNodeRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UnregisterNodeRequestMultiError, or nil if none found. +func (m *UnregisterNodeRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *UnregisterNodeRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for NodeId + + // no validation rules for Force + + if len(errors) > 0 { + return UnregisterNodeRequestMultiError(errors) + } + + return nil +} + +// UnregisterNodeRequestMultiError is an error wrapping multiple validation +// errors returned by UnregisterNodeRequest.ValidateAll() if the designated +// constraints aren't met. +type UnregisterNodeRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UnregisterNodeRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UnregisterNodeRequestMultiError) AllErrors() []error { return m } + +// UnregisterNodeRequestValidationError is the validation error returned by +// UnregisterNodeRequest.Validate if the designated constraints aren't met. +type UnregisterNodeRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UnregisterNodeRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UnregisterNodeRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UnregisterNodeRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UnregisterNodeRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UnregisterNodeRequestValidationError) ErrorName() string { + return "UnregisterNodeRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e UnregisterNodeRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUnregisterNodeRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UnregisterNodeRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UnregisterNodeRequestValidationError{} + +// Validate checks the field values on UnregisterNodeResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *UnregisterNodeResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UnregisterNodeResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UnregisterNodeResponseMultiError, or nil if none found. +func (m *UnregisterNodeResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *UnregisterNodeResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Warning + + if len(errors) > 0 { + return UnregisterNodeResponseMultiError(errors) + } + + return nil +} + +// UnregisterNodeResponseMultiError is an error wrapping multiple validation +// errors returned by UnregisterNodeResponse.ValidateAll() if the designated +// constraints aren't met. +type UnregisterNodeResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UnregisterNodeResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UnregisterNodeResponseMultiError) AllErrors() []error { return m } + +// UnregisterNodeResponseValidationError is the validation error returned by +// UnregisterNodeResponse.Validate if the designated constraints aren't met. +type UnregisterNodeResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UnregisterNodeResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UnregisterNodeResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UnregisterNodeResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UnregisterNodeResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UnregisterNodeResponseValidationError) ErrorName() string { + return "UnregisterNodeResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e UnregisterNodeResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUnregisterNodeResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UnregisterNodeResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UnregisterNodeResponseValidationError{} diff --git a/api/managementpb/node.proto b/api/managementpb/node.proto index aa1fe7c53d..b9e88e7ccc 100644 --- a/api/managementpb/node.proto +++ b/api/managementpb/node.proto @@ -58,9 +58,21 @@ message RegisterNodeResponse { string warning = 5; } +message UnregisterNodeRequest { + // Node_id to be unregistered. + string node_id = 1; + // Force delete node, related service account, even if it has more service tokens attached. + bool force = 2; +} + +message UnregisterNodeResponse { + // Warning message if there are more service tokens attached to service account. + string warning = 1; +} + // Node service provides public Management API methods for Nodes. service Node { - // RegisterNode registers a new Node and pmm-agent. + // RegisterNode registers a new Node, pmm-agent and create service account and it's token. rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse) { option (google.api.http) = { post: "/v1/management/Node/Register" @@ -71,4 +83,15 @@ service Node { description: "Registers a new Node and pmm-agent." }; } + // UnregisterNode unregister a Node, pmm-agent and remove service account and it's token. + rpc UnregisterNode(UnregisterNodeRequest) returns (UnregisterNodeResponse) { + option (google.api.http) = { + post: "/v1/management/Node/Unregister" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Unregister Node" + description: "Unregister a Node and pmm-agent" + }; + } } diff --git a/api/managementpb/node_grpc.pb.go b/api/managementpb/node_grpc.pb.go index ee9850d2aa..6863519d80 100644 --- a/api/managementpb/node_grpc.pb.go +++ b/api/managementpb/node_grpc.pb.go @@ -20,15 +20,18 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - Node_RegisterNode_FullMethodName = "/management.Node/RegisterNode" + Node_RegisterNode_FullMethodName = "/management.Node/RegisterNode" + Node_UnregisterNode_FullMethodName = "/management.Node/UnregisterNode" ) // NodeClient is the client API for Node service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type NodeClient interface { - // RegisterNode registers a new Node and pmm-agent. + // RegisterNode registers a new Node, pmm-agent and create service account and it's token. RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) + // UnregisterNode unregister a Node, pmm-agent and remove service account and it's token. + UnregisterNode(ctx context.Context, in *UnregisterNodeRequest, opts ...grpc.CallOption) (*UnregisterNodeResponse, error) } type nodeClient struct { @@ -48,12 +51,23 @@ func (c *nodeClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, return out, nil } +func (c *nodeClient) UnregisterNode(ctx context.Context, in *UnregisterNodeRequest, opts ...grpc.CallOption) (*UnregisterNodeResponse, error) { + out := new(UnregisterNodeResponse) + err := c.cc.Invoke(ctx, Node_UnregisterNode_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // NodeServer is the server API for Node service. // All implementations must embed UnimplementedNodeServer // for forward compatibility type NodeServer interface { - // RegisterNode registers a new Node and pmm-agent. + // RegisterNode registers a new Node, pmm-agent and create service account and it's token. RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) + // UnregisterNode unregister a Node, pmm-agent and remove service account and it's token. + UnregisterNode(context.Context, *UnregisterNodeRequest) (*UnregisterNodeResponse, error) mustEmbedUnimplementedNodeServer() } @@ -63,6 +77,10 @@ type UnimplementedNodeServer struct{} func (UnimplementedNodeServer) RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RegisterNode not implemented") } + +func (UnimplementedNodeServer) UnregisterNode(context.Context, *UnregisterNodeRequest) (*UnregisterNodeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnregisterNode not implemented") +} func (UnimplementedNodeServer) mustEmbedUnimplementedNodeServer() {} // UnsafeNodeServer may be embedded to opt out of forward compatibility for this service. @@ -94,6 +112,24 @@ func _Node_RegisterNode_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } +func _Node_UnregisterNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnregisterNodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NodeServer).UnregisterNode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Node_UnregisterNode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NodeServer).UnregisterNode(ctx, req.(*UnregisterNodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Node_ServiceDesc is the grpc.ServiceDesc for Node service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -105,6 +141,10 @@ var Node_ServiceDesc = grpc.ServiceDesc{ MethodName: "RegisterNode", Handler: _Node_RegisterNode_Handler, }, + { + MethodName: "UnregisterNode", + Handler: _Node_UnregisterNode_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "managementpb/node.proto", diff --git a/api/swagger/swagger-dev.json b/api/swagger/swagger-dev.json index 575579b963..d25823f48e 100644 --- a/api/swagger/swagger-dev.json +++ b/api/swagger/swagger-dev.json @@ -22340,6 +22340,84 @@ } } }, + "/v1/management/Node/Unregister": { + "post": { + "description": "Unregister a Node and pmm-agent", + "tags": [ + "Node" + ], + "summary": "Unregister Node", + "operationId": "UnregisterNode", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "node_id": { + "description": "Node_id to be unregistered.", + "type": "string", + "x-order": 0 + }, + "force": { + "description": "Force delete node, related service account, even if it has more service tokens attached.", + "type": "boolean", + "x-order": 1 + } + } + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": { + "warning": { + "description": "Warning message if there are more service tokens attached to service account.", + "type": "string", + "x-order": 0 + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "x-order": 0 + }, + "message": { + "type": "string", + "x-order": 1 + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "x-order": 0 + } + }, + "additionalProperties": false + }, + "x-order": 2 + } + } + } + } + } + } + }, "/v1/management/PostgreSQL/Add": { "post": { "description": "Adds PostgreSQL Service and starts postgres exporter. It automatically adds a service to inventory, which is running on provided \"node_id\", then adds \"postgres_exporter\" with provided \"pmm_agent_id\" and other parameters.", diff --git a/api/swagger/swagger.json b/api/swagger/swagger.json index 7d9e31f0eb..d35874e353 100644 --- a/api/swagger/swagger.json +++ b/api/swagger/swagger.json @@ -18662,6 +18662,84 @@ } } }, + "/v1/management/Node/Unregister": { + "post": { + "description": "Unregister a Node and pmm-agent", + "tags": [ + "Node" + ], + "summary": "Unregister Node", + "operationId": "UnregisterNode", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "node_id": { + "description": "Node_id to be unregistered.", + "type": "string", + "x-order": 0 + }, + "force": { + "description": "Force delete node, related service account, even if it has more service tokens attached.", + "type": "boolean", + "x-order": 1 + } + } + } + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": { + "warning": { + "description": "Warning message if there are more service tokens attached to service account.", + "type": "string", + "x-order": 0 + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "x-order": 0 + }, + "message": { + "type": "string", + "x-order": 1 + }, + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "x-order": 0 + } + }, + "additionalProperties": false + }, + "x-order": 2 + } + } + } + } + } + } + }, "/v1/management/PostgreSQL/Add": { "post": { "description": "Adds PostgreSQL Service and starts postgres exporter. It automatically adds a service to inventory, which is running on provided \"node_id\", then adds \"postgres_exporter\" with provided \"pmm_agent_id\" and other parameters.", diff --git a/docs/api/welcome/authentication.md b/docs/api/welcome/authentication.md index 6693f8765d..cec8d37873 100644 --- a/docs/api/welcome/authentication.md +++ b/docs/api/welcome/authentication.md @@ -12,7 +12,7 @@ PMM Server's user authentication is built on top of and is compatible with [Graf In this section we will talk about two main authentication mechanisms: - Basic HTTP authentication -- Bearer Authentication (Authentication with an API key) +- Bearer Authentication (Authentication with an Service Token or API key) ### Basic HTTP Authentication @@ -37,8 +37,23 @@ curl -X GET -u admin:admin -H 'Content-Type: application/json' https://127.0.0.1 ### Bearer Authentication -Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens. The bearer token is a cryptic API key, which can be generated by the server admin from the Settings UI or via a respective API call (read more about how to generate an API key). The client must send the API key in the `Authorization` header when making requests to protected resources: +Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens. The bearer token is a cryptic Service Token, or API key, which can be generated by the server admin from the Settings UI or via a respective API call (read more about how to generate Service token or API key). The client must send the Service Token/API key in the `Authorization` header when making requests to protected resources: +Service Token example: +```shell +curl -X GET -H 'Authorization: Bearer glsa_Fp0ggev31R58ueNJbJgYw7fIGfO3yKWH_746383ab' \ + -H 'Content-Type: application/json' https://127.0.0.1/v1/version +``` + +You can use the Service Token in basic authentication as well: + +```shell +curl -X GET -H 'Content-Type: application/json' \ + https://service_token:glsa_Fp0ggev31R58ueNJbJgYw7fIGfO3yKWH_746383ab@127.0.0.1/v1/version +``` +Service Token usually has "glsa_" prefix. If you convert it from API key, it will not change. If you use API key, then it will be automatically converted into Service Account and Service Token. + +API key example: ```shell curl -X GET -H 'Authorization: Bearer eyJrIjoiUXRkeDNMS1g1bFVyY0tUj1o0SmhBc3g4QUdTRVAwekoiLCJuIjoicG1tLXRlc3QiLCJpZCI6MX0=' \ -H 'Content-Type: application/json' https://127.0.0.1/v1/version diff --git a/go.mod b/go.mod index 98934ea3c8..8b608de57d 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/go-openapi/swag v0.23.0 github.com/go-openapi/validate v0.24.0 github.com/go-sql-driver/mysql v1.7.1 + github.com/gogo/status v1.1.1 github.com/golang-migrate/migrate/v4 v4.17.0 github.com/golang/protobuf v1.5.3 github.com/google/uuid v1.6.0 @@ -92,6 +93,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a // indirect github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/google/btree v1.0.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect diff --git a/go.sum b/go.sum index b04e515a4b..b0ecfaa9bc 100644 --- a/go.sum +++ b/go.sum @@ -221,10 +221,14 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a h1:dR8+Q0uO5S2ZBcs2IH6VBKYwSxPo2vYCYq0ot0mu7xA= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= +github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= @@ -899,6 +903,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -935,6 +940,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1: google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/managed/services/grafana/auth_server.go b/managed/services/grafana/auth_server.go index e04ea1937f..2050962b73 100644 --- a/managed/services/grafana/auth_server.go +++ b/managed/services/grafana/auth_server.go @@ -38,10 +38,13 @@ import ( "github.com/percona/pmm/managed/models" ) +const ( + connectionEndpoint = "/agent.Agent/Connect" +) + // rules maps original URL prefix to minimal required role. var rules = map[string]role{ - // TODO https://jira.percona.com/browse/PMM-4420 - "/agent.Agent/Connect": none, + connectionEndpoint: admin, "/inventory.": admin, "/management.": admin, @@ -459,6 +462,17 @@ func nextPrefix(path string) string { return path[:i+1] } +func isLocalAgentConnection(req *http.Request) bool { + ip := strings.Split(req.RemoteAddr, ":")[0] + pmmAgent := req.Header.Get("Pmm-Agent-Id") + path := req.Header.Get("X-Original-Uri") + if ip == "127.0.0.1" && pmmAgent == "pmm-server" && path == connectionEndpoint { + return true + } + + return false +} + // authenticate checks if user has access to a specific path. // It returns user information retrieved during authentication. // Paths which require no Grafana role return zero value for @@ -498,22 +512,30 @@ func (s *AuthServer) authenticate(ctx context.Context, req *http.Request, l *log return nil, nil } - // Get authenticated user from Grafana - authUser, authErr := s.getAuthUser(ctx, req, l) - if authErr != nil { - return nil, authErr + var user *authUser + if isLocalAgentConnection(req) { + user = &authUser{ + role: rules[connectionEndpoint], + userID: 0, + } + } else { + var authErr *authError + // Get authenticated user from Grafana + user, authErr = s.getAuthUser(ctx, req, l) + if authErr != nil { + return nil, authErr + } } + l = l.WithField("role", user.role.String()) - l = l.WithField("role", authUser.role.String()) - - if authUser.role == grafanaAdmin { + if user.role == grafanaAdmin { l.Debugf("Grafana admin, allowing access.") - return authUser, nil + return user, nil } - if minRole <= authUser.role { + if minRole <= user.role { l.Debugf("Minimal required role is %q, granting access.", minRole) - return authUser, nil + return user, nil } l.Warnf("Minimal required role is %q.", minRole) diff --git a/managed/services/grafana/auth_server_test.go b/managed/services/grafana/auth_server_test.go index 15822094b4..0b1bbbe14c 100644 --- a/managed/services/grafana/auth_server_test.go +++ b/managed/services/grafana/auth_server_test.go @@ -31,6 +31,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "gopkg.in/reform.v1" "gopkg.in/reform.v1/dialects/postgresql" @@ -161,7 +162,6 @@ func TestAuthServerMustSetup(t *testing.T) { func TestAuthServerAuthenticate(t *testing.T) { t.Parallel() - // logrus.SetLevel(logrus.TraceLevel) checker := &mockAwsInstanceChecker{} checker.Test(t) @@ -198,7 +198,7 @@ func TestAuthServerAuthenticate(t *testing.T) { }) for uri, minRole := range map[string]role{ - "/agent.Agent/Connect": none, + "/agent.Agent/Connect": admin, "/inventory.Nodes/ListNodes": admin, "/management.Actions/StartMySQLShowTableStatusAction": viewer, @@ -270,6 +270,75 @@ func TestAuthServerAuthenticate(t *testing.T) { } } +func TestServerClientConnection(t *testing.T) { + t.Parallel() + + checker := &mockAwsInstanceChecker{} + checker.Test(t) + t.Cleanup(func() { checker.AssertExpectations(t) }) + + ctx := context.Background() + c := NewClient("127.0.0.1:3000") + s := NewAuthServer(c, checker, nil) + + t.Run("Basic auth - success", func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil) + require.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + _, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name())) + assert.Nil(t, authError) + }) + + t.Run("Basic auth - fail", func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil) + require.NoError(t, err) + req.SetBasicAuth("admin", "wrong") + + _, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name())) + assert.Equal(t, codes.Unauthenticated, authError.code) + }) + + t.Run("Token auth - success", func(t *testing.T) { + t.Parallel() + + nodeName := fmt.Sprintf("N1-%d", time.Now().UnixNano()) + headersMD := metadata.New(map[string]string{ + "Authorization": "Basic YWRtaW46YWRtaW4=", + }) + ctx := metadata.NewIncomingContext(context.Background(), headersMD) + _, serviceToken, err := c.CreateServiceAccount(ctx, nodeName, true) + require.NoError(t, err) + defer func() { + warning, err := c.DeleteServiceAccount(ctx, nodeName, true) + require.NoError(t, err) + require.Empty(t, warning) + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil) + require.NoError(t, err) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", serviceToken)) + + _, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name())) + assert.Nil(t, authError) + }) + + t.Run("Token auth - fail", func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer wrong") + + _, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name())) + assert.Equal(t, codes.Unauthenticated, authError.code) + }) +} + func TestAuthServerAddVMGatewayToken(t *testing.T) { ctx := logger.Set(context.Background(), t.Name()) uuid.SetRand(&tests.IDReader{}) diff --git a/managed/services/grafana/client.go b/managed/services/grafana/client.go index 915f376083..724eb01388 100644 --- a/managed/services/grafana/client.go +++ b/managed/services/grafana/client.go @@ -19,7 +19,6 @@ package grafana import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -39,13 +38,18 @@ import ( "google.golang.org/grpc/status" "github.com/percona/pmm/managed/services" + "github.com/percona/pmm/managed/utils/auth" "github.com/percona/pmm/managed/utils/irt" ) // ErrFailedToGetToken means it failed to get user's token. Most likely due to the fact user is not logged in using Percona Account. var ErrFailedToGetToken = errors.New("failed to get token") -const defaultEvaluationInterval = time.Minute +const ( + defaultEvaluationInterval = time.Minute + pmmServiceTokenName = "pmm-agent-service-token" //nolint:gosec + pmmServiceAccountName = "pmm-agent-service-account" //nolint:gosec +) // Client represents a client for Grafana API. type Client struct { @@ -136,7 +140,7 @@ func (c *Client) do(ctx context.Context, method, path, rawQuery string, headers if err != nil { return errors.WithStack(err) } - if resp.StatusCode != 200 && resp.StatusCode != 202 { + if resp.StatusCode < 200 || resp.StatusCode > 202 { cErr := &clientError{ Method: req.Method, URL: req.URL.String(), @@ -192,7 +196,7 @@ func (r role) String() string { // GetUserID returns user ID from Grafana for the current user. func (c *Client) GetUserID(ctx context.Context) (int, error) { - authHeaders, err := c.authHeadersFromContext(ctx) + authHeaders, err := auth.GetHeadersFromContext(ctx) if err != nil { return 0, err } @@ -211,27 +215,41 @@ func (c *Client) GetUserID(ctx context.Context) (int, error) { return int(userID), nil } +var emptyUser = authUser{ + role: none, + userID: 0, +} + // getAuthUser returns grafanaAdmin if currently authenticated user is a Grafana (super) admin. // Otherwise, it returns a role in the default organization (with ID 1). // Ctx is used only for cancelation. func (c *Client) getAuthUser(ctx context.Context, authHeaders http.Header) (authUser, error) { - // Check if it's API Key - if c.IsAPIKeyAuth(authHeaders) { - role, err := c.getRoleForAPIKey(ctx, authHeaders) + // Check if API Key or Service Token is authorized. + token := auth.GetTokenFromHeaders(authHeaders) + if token != "" { + role, err := c.getRoleForServiceToken(ctx, token) + if err != nil { + if strings.Contains(err.Error(), "Auth method is not service account token") { + role, err := c.getRoleForAPIKey(ctx, authHeaders) + return authUser{ + role: role, + userID: 0, + }, err + } + + return emptyUser, err + } return authUser{ role: role, userID: 0, - }, err + }, nil } // https://grafana.com/docs/http_api/user/#actual-user - works only with Basic Auth var m map[string]interface{} err := c.do(ctx, http.MethodGet, "/api/user", "", authHeaders, nil, &m) if err != nil { - return authUser{ - role: none, - userID: 0, - }, err + return emptyUser, err } id, _ := m["id"].(float64) @@ -274,21 +292,18 @@ func (c *Client) getAuthUser(ctx context.Context, authHeaders http.Header) (auth }, nil } -// IsAPIKeyAuth checks if the request is made using an API Key. -func (c *Client) IsAPIKeyAuth(headers http.Header) bool { - authHeader := headers.Get("Authorization") - switch { - case strings.HasPrefix(authHeader, "Bearer"): - return true - case strings.HasPrefix(authHeader, "Basic"): - h := strings.TrimPrefix(authHeader, "Basic") - d, err := base64.StdEncoding.DecodeString(strings.TrimSpace(h)) - if err != nil { - return false - } - return strings.HasPrefix(string(d), "api_key:") +func (c *Client) getRoleForAPIKey(ctx context.Context, authHeaders http.Header) (role, error) { + var k map[string]interface{} + if err := c.do(ctx, http.MethodGet, "/api/auth/key", "", authHeaders, nil, &k); err != nil { + return none, err + } + + if id, _ := k["orgId"].(float64); id != 1 { + return none, nil } - return false + + role, _ := k["role"].(string) + return c.convertRole(role), nil } func (c *Client) convertRole(role string) role { @@ -312,9 +327,12 @@ type apiKey struct { Expiration *time.Time `json:"expiration,omitempty"` } -func (c *Client) getRoleForAPIKey(ctx context.Context, authHeaders http.Header) (role, error) { +func (c *Client) getRoleForServiceToken(ctx context.Context, token string) (role, error) { + header := http.Header{} + header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + var k map[string]interface{} - if err := c.do(ctx, http.MethodGet, "/api/auth/key", "", authHeaders, nil, &k); err != nil { + if err := c.do(ctx, http.MethodGet, "/api/auth/serviceaccount", "", header, nil, &k); err != nil { return none, err } @@ -326,6 +344,54 @@ func (c *Client) getRoleForAPIKey(ctx context.Context, authHeaders http.Header) return c.convertRole(role), nil } +type serviceAccountSearch struct { + TotalCount int `json:"totalCount"` + ServiceAccounts []serviceAccount `json:"serviceAccounts"` +} + +func (c *Client) getServiceAccountIDFromName(ctx context.Context, nodeName string, authHeaders http.Header) (int, error) { + var res serviceAccountSearch + serviceAccountName := fmt.Sprintf("%s-%s", pmmServiceAccountName, nodeName) + if err := c.do(ctx, http.MethodGet, "/api/serviceaccounts/search", fmt.Sprintf("query=%s", serviceAccountName), authHeaders, nil, &res); err != nil { + return 0, err + } + for _, serviceAccount := range res.ServiceAccounts { + if serviceAccount.Name != serviceAccountName { + continue + } + return serviceAccount.ID, nil + } + + return 0, errors.Errorf("service account %s not found", serviceAccountName) +} + +func (c *Client) getNotPMMAgentTokenCountForServiceAccount(ctx context.Context, nodeName string) (int, error) { + authHeaders, err := auth.GetHeadersFromContext(ctx) + if err != nil { + return 0, err + } + + serviceAccountID, err := c.getServiceAccountIDFromName(ctx, nodeName, authHeaders) + if err != nil { + return 0, err + } + + var tokens []serviceToken + if err := c.do(ctx, http.MethodGet, fmt.Sprintf("/api/serviceaccounts/%d/tokens", serviceAccountID), "", authHeaders, nil, &tokens); err != nil { + return 0, err + } + + count := 0 + for _, token := range tokens { + serviceTokenName := fmt.Sprintf("%s-%s", pmmServiceTokenName, nodeName) + if !strings.HasPrefix(token.Name, serviceTokenName) { + count++ + } + } + + return count, nil +} + func (c *Client) testCreateUser(ctx context.Context, login string, role role, authHeaders http.Header) (int, error) { // https://grafana.com/docs/http_api/admin/#global-users b, err := json.Marshal(map[string]string{ @@ -366,50 +432,60 @@ func (c *Client) testDeleteUser(ctx context.Context, userID int, authHeaders htt return c.do(ctx, "DELETE", "/api/admin/users/"+strconv.Itoa(userID), "", authHeaders, nil, nil) } -// CreateAdminAPIKey creates API key with Admin role and provided name. -func (c *Client) CreateAdminAPIKey(ctx context.Context, name string) (int64, string, error) { - authHeaders, err := c.authHeadersFromContext(ctx) +// CreateServiceAccount creates service account and token with Admin role. +func (c *Client) CreateServiceAccount(ctx context.Context, nodeName string, reregister bool) (int, string, error) { + authHeaders, err := auth.GetHeadersFromContext(ctx) if err != nil { return 0, "", err } - return c.createAPIKey(ctx, name, admin, authHeaders) -} -// DeleteAPIKeysWithPrefix deletes all API keys with provided prefix. If there is no api key with provided prefix just ignores it. -func (c *Client) DeleteAPIKeysWithPrefix(ctx context.Context, prefix string) error { - authHeaders, err := c.authHeadersFromContext(ctx) + serviceAccountID, err := c.createServiceAccount(ctx, admin, nodeName, reregister, authHeaders) if err != nil { - return err - } - var keys []apiKey - if err := c.do(ctx, http.MethodGet, "/api/auth/keys", "", authHeaders, nil, &keys); err != nil { - return err + return 0, "", err } - for _, k := range keys { - if strings.HasPrefix(k.Name, prefix) { - err := c.deleteAPIKey(ctx, k.ID, authHeaders) - if err != nil { - return err - } - } + _, serviceToken, err := c.createServiceToken(ctx, serviceAccountID, nodeName, reregister, authHeaders) + if err != nil { + return 0, "", err } - return nil + return serviceAccountID, serviceToken, nil } -// DeleteAPIKeyByID deletes API key by ID. -func (c *Client) DeleteAPIKeyByID(ctx context.Context, id int64) error { - authHeaders, err := c.authHeadersFromContext(ctx) +// DeleteServiceAccount deletes service account by current service token. +func (c *Client) DeleteServiceAccount(ctx context.Context, nodeName string, force bool) (string, error) { + authHeaders, err := auth.GetHeadersFromContext(ctx) if err != nil { - return err + return "", err + } + + warning := "" + serviceAccountID, err := c.getServiceAccountIDFromName(ctx, nodeName, authHeaders) + if err != nil { + return warning, err + } + + customsTokensCount, err := c.getNotPMMAgentTokenCountForServiceAccount(ctx, nodeName) + if err != nil { + return warning, err + } + + if !force && customsTokensCount > 0 { + warning = "Service account wont be deleted, because there are more not PMM agent related service tokens." + err = c.deletePMMAgentServiceToken(ctx, serviceAccountID, nodeName, authHeaders) + } else { + err = c.deleteServiceAccount(ctx, serviceAccountID, authHeaders) + } + if err != nil { + return warning, err } - return c.deleteAPIKey(ctx, id, authHeaders) + + return warning, err } // CreateAlertRule creates Grafana alert rule. func (c *Client) CreateAlertRule(ctx context.Context, folderName, groupName string, rule *services.Rule) error { - authHeaders, err := c.authHeadersFromContext(ctx) + authHeaders, err := auth.GetHeadersFromContext(ctx) if err != nil { return err } @@ -541,7 +617,7 @@ func (c *Client) GetFolderByUID(ctx context.Context, uid string) (*gapi.Folder, } func (c *Client) createGrafanaClient(ctx context.Context) (*gapi.Client, error) { - authHeaders, err := c.authHeadersFromContext(ctx) + authHeaders, err := auth.GetHeadersFromContext(ctx) if err != nil { return nil, errors.WithStack(err) } @@ -559,30 +635,6 @@ func (c *Client) createGrafanaClient(ctx context.Context) (*gapi.Client, error) return grafanaClient, nil } -func (c *Client) authHeadersFromContext(ctx context.Context) (http.Header, error) { - headers, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, fmt.Errorf("cannot get headers from metadata") - } - // get authorization from headers. - authorizationHeaders := headers.Get("Authorization") - cookieHeaders := headers.Get("grpcgateway-cookie") - if len(authorizationHeaders) == 0 && len(cookieHeaders) == 0 { - return nil, status.Error(codes.Unauthenticated, "Authorization error.") - } - - authHeaders := make(http.Header) - if len(authorizationHeaders) != 0 { - authHeaders.Add("Authorization", authorizationHeaders[0]) - } - if len(cookieHeaders) != 0 { - for _, header := range cookieHeaders { - authHeaders.Add("Cookie", header) - } - } - return authHeaders, nil -} - func (c *Client) createAPIKey(ctx context.Context, name string, role role, authHeaders http.Header) (int64, string, error) { // https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-key b, err := json.Marshal(apiKey{Name: name, Role: role.String()}) @@ -612,6 +664,117 @@ func (c *Client) deleteAPIKey(ctx context.Context, apiKeyID int64, authHeaders h return c.do(ctx, "DELETE", "/api/auth/keys/"+strconv.FormatInt(apiKeyID, 10), "", authHeaders, nil, nil) } +type serviceAccount struct { + ID int `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + Force bool `json:"force"` +} +type serviceToken struct { + ID int `json:"id"` + Name string `json:"name"` + Role string `json:"role"` +} + +func (c *Client) createServiceAccount(ctx context.Context, role role, nodeName string, reregister bool, authHeaders http.Header) (int, error) { + if role == none { + return 0, errors.New("you cannot create service account with empty role") + } + + // Max length of service account name is 190 chars (default limit in Grafana). In reality it is 187. + // Some test could fail if you will have name longer than 187 chars. + serviceAccountName := fmt.Sprintf("%s-%s", pmmServiceAccountName, nodeName) + b, err := json.Marshal(serviceAccount{Name: serviceAccountName, Role: role.String(), Force: reregister}) + if err != nil { + return 0, errors.WithStack(err) + } + + var m map[string]interface{} + if err = c.do(ctx, "POST", "/api/serviceaccounts", "", authHeaders, b, &m); err != nil { + return 0, err + } + + serviceAccountID := int(m["id"].(float64)) //nolint:forcetypeassert + + // orgId is ignored during creating service account and default is -1 + // orgId should be set to 1 + if err = c.do(ctx, "PATCH", fmt.Sprintf("/api/serviceaccounts/%d", serviceAccountID), "", authHeaders, []byte("{\"orgId\": 1}"), &m); err != nil { + return 0, err + } + + return serviceAccountID, nil +} + +func (c *Client) createServiceToken(ctx context.Context, serviceAccountID int, nodeName string, reregister bool, authHeaders http.Header) (int, string, error) { + serviceTokenName := fmt.Sprintf("%s-%s", pmmServiceTokenName, nodeName) + exists, err := c.serviceTokenExists(ctx, serviceAccountID, nodeName, authHeaders) + if err != nil { + return 0, "", err + } + if exists && reregister { + err := c.deletePMMAgentServiceToken(ctx, serviceAccountID, nodeName, authHeaders) + if err != nil { + return 0, "", err + } + } + + b, err := json.Marshal(serviceToken{Name: serviceTokenName, Role: admin.String()}) + if err != nil { + return 0, "", errors.WithStack(err) + } + + var m map[string]interface{} + if err = c.do(ctx, "POST", fmt.Sprintf("/api/serviceaccounts/%d/tokens", serviceAccountID), "", authHeaders, b, &m); err != nil { + return 0, "", err + } + serviceTokenID := int(m["id"].(float64)) //nolint:forcetypeassert + serviceTokenKey := m["key"].(string) //nolint:forcetypeassert + + return serviceTokenID, serviceTokenKey, nil +} + +func (c *Client) serviceTokenExists(ctx context.Context, serviceAccountID int, nodeName string, authHeaders http.Header) (bool, error) { + var tokens []serviceToken + if err := c.do(ctx, "GET", fmt.Sprintf("/api/serviceaccounts/%d/tokens", serviceAccountID), "", authHeaders, nil, &tokens); err != nil { + return false, err + } + + serviceTokenName := fmt.Sprintf("%s-%s", pmmServiceTokenName, nodeName) + for _, token := range tokens { + if !strings.HasPrefix(token.Name, serviceTokenName) { + continue + } + + return true, nil + } + + return false, nil +} + +func (c *Client) deletePMMAgentServiceToken(ctx context.Context, serviceAccountID int, nodeName string, authHeaders http.Header) error { + var tokens []serviceToken + if err := c.do(ctx, "GET", fmt.Sprintf("/api/serviceaccounts/%d/tokens", serviceAccountID), "", authHeaders, nil, &tokens); err != nil { + return err + } + + serviceTokenName := fmt.Sprintf("%s-%s", pmmServiceTokenName, nodeName) + for _, token := range tokens { + if strings.HasPrefix(token.Name, serviceTokenName) { + if err := c.do(ctx, "DELETE", fmt.Sprintf("/api/serviceaccounts/%d/tokens/%d", serviceAccountID, token.ID), "", authHeaders, nil, nil); err != nil { + return err + } + + return nil + } + } + + return nil +} + +func (c *Client) deleteServiceAccount(ctx context.Context, serviceAccountID int, authHeaders http.Header) error { + return c.do(ctx, "DELETE", fmt.Sprintf("/api/serviceaccounts/%d", serviceAccountID), "", authHeaders, nil, nil) +} + // Annotation contains grafana annotation response. type annotation struct { Time time.Time `json:"-"` diff --git a/managed/services/grafana/client_test.go b/managed/services/grafana/client_test.go index 16cbd03dc0..66963ded67 100644 --- a/managed/services/grafana/client_test.go +++ b/managed/services/grafana/client_test.go @@ -72,9 +72,6 @@ func TestClient(t *testing.T) { }) t.Run("NewUserViewerByDefault", func(t *testing.T) { - // do not run this test in parallel - they lock Grafana's sqlite3 database - // t.Parallel() - // See [users] in grafana.ini. login := fmt.Sprintf("%s-%d", none, time.Now().Nanosecond()) @@ -104,8 +101,7 @@ func TestClient(t *testing.T) { role := role t.Run(fmt.Sprintf("Basic auth %s", role.String()), func(t *testing.T) { - // do not run this test in parallel - they lock Grafana's sqlite3 database - // t.Parallel() + t.Parallel() login := fmt.Sprintf("basic-%s-%d", role, time.Now().Nanosecond()) userID, err := c.testCreateUser(ctx, login, role, authHeaders) @@ -131,8 +127,7 @@ func TestClient(t *testing.T) { }) t.Run(fmt.Sprintf("API Key auth %s", role.String()), func(t *testing.T) { - // do not run this test in parallel - they lock Grafana's sqlite3 database - // t.Parallel() + t.Parallel() login := fmt.Sprintf("api-%s-%d", role, time.Now().Nanosecond()) apiKeyID, apiKey, err := c.createAPIKey(ctx, login, role, authHeaders) @@ -155,6 +150,35 @@ func TestClient(t *testing.T) { assert.Equal(t, role, actualRole) assert.Equal(t, role.String(), actualRole.String()) }) + + t.Run(fmt.Sprintf("Service token auth %s", role.String()), func(t *testing.T) { + t.Parallel() + + nodeName := fmt.Sprintf("test-node-%s", role) + serviceAccountID, err := c.createServiceAccount(ctx, role, nodeName, true, authHeaders) + require.NoError(t, err) + defer func() { + err := c.deleteServiceAccount(ctx, serviceAccountID, authHeaders) + require.NoError(t, err) + }() + + serviceTokenID, serviceToken, err := c.createServiceToken(ctx, serviceAccountID, nodeName, true, authHeaders) + require.NoError(t, err) + require.NotZero(t, serviceTokenID) + require.NotEmpty(t, serviceToken) + defer func() { + err := c.deletePMMAgentServiceToken(ctx, serviceAccountID, nodeName, authHeaders) + require.NoError(t, err) + }() + + serviceTokenAuthHeaders := http.Header{} + serviceTokenAuthHeaders.Set("Authorization", fmt.Sprintf("Bearer %s", serviceToken)) + u, err := c.getAuthUser(ctx, serviceTokenAuthHeaders) + assert.NoError(t, err) + actualRole := u.role + assert.Equal(t, role, actualRole) + assert.Equal(t, role.String(), actualRole.String()) + }) } }) diff --git a/managed/services/management/deps.go b/managed/services/management/deps.go index 0da0dffe03..500a3bf039 100644 --- a/managed/services/management/deps.go +++ b/managed/services/management/deps.go @@ -17,7 +17,6 @@ package management import ( "context" - "net/http" "time" "github.com/percona-platform/saas/pkg/check" @@ -99,7 +98,7 @@ type victoriaMetricsClient interface { Query(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) } -type apiKeyProvider interface { - CreateAdminAPIKey(ctx context.Context, name string) (int64, string, error) - IsAPIKeyAuth(headers http.Header) bool +type authProvider interface { + CreateServiceAccount(ctx context.Context, noneName string, reregister bool) (int, string, error) + DeleteServiceAccount(ctx context.Context, noneName string, force bool) (string, error) } diff --git a/managed/services/management/grpc/node_server.go b/managed/services/management/grpc/node_server.go index 1191ee3081..0136fa8195 100644 --- a/managed/services/management/grpc/node_server.go +++ b/managed/services/management/grpc/node_server.go @@ -38,3 +38,8 @@ func NewManagementNodeServer(s *management.NodeService) managementpb.NodeServer func (s *nodeServer) RegisterNode(ctx context.Context, req *managementpb.RegisterNodeRequest) (*managementpb.RegisterNodeResponse, error) { return s.svc.Register(ctx, req) } + +// UnregisterNode do unregistration of the Node. +func (s *nodeServer) UnregisterNode(ctx context.Context, req *managementpb.UnregisterNodeRequest) (*managementpb.UnregisterNodeResponse, error) { + return s.svc.Unregister(ctx, req) +} diff --git a/managed/services/management/mock_api_key_provider_test.go b/managed/services/management/mock_api_key_provider_test.go deleted file mode 100644 index f7aed1af97..0000000000 --- a/managed/services/management/mock_api_key_provider_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package management - -import ( - context "context" - http "net/http" - - mock "github.com/stretchr/testify/mock" -) - -// mockApiKeyProvider is an autogenerated mock type for the apiKeyProvider type -type mockApiKeyProvider struct { - mock.Mock -} - -// CreateAdminAPIKey provides a mock function with given fields: ctx, name -func (_m *mockApiKeyProvider) CreateAdminAPIKey(ctx context.Context, name string) (int64, string, error) { - ret := _m.Called(ctx, name) - - if len(ret) == 0 { - panic("no return value specified for CreateAdminAPIKey") - } - - var r0 int64 - var r1 string - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string) (int64, string, error)); ok { - return rf(ctx, name) - } - if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok { - r0 = rf(ctx, name) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, string) string); ok { - r1 = rf(ctx, name) - } else { - r1 = ret.Get(1).(string) - } - - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(ctx, name) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// IsAPIKeyAuth provides a mock function with given fields: headers -func (_m *mockApiKeyProvider) IsAPIKeyAuth(headers http.Header) bool { - ret := _m.Called(headers) - - if len(ret) == 0 { - panic("no return value specified for IsAPIKeyAuth") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(http.Header) bool); ok { - r0 = rf(headers) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// newMockApiKeyProvider creates a new instance of mockApiKeyProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockApiKeyProvider(t interface { - mock.TestingT - Cleanup(func()) -}, -) *mockApiKeyProvider { - mock := &mockApiKeyProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/managed/services/management/mock_auth_provider_test.go b/managed/services/management/mock_auth_provider_test.go new file mode 100644 index 0000000000..a00b0dd60b --- /dev/null +++ b/managed/services/management/mock_auth_provider_test.go @@ -0,0 +1,92 @@ +// Code generated by mockery. DO NOT EDIT. + +package management + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// mockAuthProvider is an autogenerated mock type for the authProvider type +type mockAuthProvider struct { + mock.Mock +} + +// CreateServiceAccount provides a mock function with given fields: ctx, noneName, reregister +func (_m *mockAuthProvider) CreateServiceAccount(ctx context.Context, noneName string, reregister bool) (int, string, error) { + ret := _m.Called(ctx, noneName, reregister) + + if len(ret) == 0 { + panic("no return value specified for CreateServiceAccount") + } + + var r0 int + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, bool) (int, string, error)); ok { + return rf(ctx, noneName, reregister) + } + if rf, ok := ret.Get(0).(func(context.Context, string, bool) int); ok { + r0 = rf(ctx, noneName, reregister) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, bool) string); ok { + r1 = rf(ctx, noneName, reregister) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(context.Context, string, bool) error); ok { + r2 = rf(ctx, noneName, reregister) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// DeleteServiceAccount provides a mock function with given fields: ctx, noneName, force +func (_m *mockAuthProvider) DeleteServiceAccount(ctx context.Context, noneName string, force bool) (string, error) { + ret := _m.Called(ctx, noneName, force) + + if len(ret) == 0 { + panic("no return value specified for DeleteServiceAccount") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, bool) (string, error)); ok { + return rf(ctx, noneName, force) + } + if rf, ok := ret.Get(0).(func(context.Context, string, bool) string); ok { + r0 = rf(ctx, noneName, force) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, noneName, force) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// newMockAuthProvider creates a new instance of mockAuthProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockAuthProvider(t interface { + mock.TestingT + Cleanup(func()) +}, +) *mockAuthProvider { + mock := &mockAuthProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/managed/services/management/node.go b/managed/services/management/node.go index 26f3c54381..b685ec3939 100644 --- a/managed/services/management/node.go +++ b/managed/services/management/node.go @@ -17,14 +17,10 @@ package management import ( "context" - "fmt" - "math/rand" - "net/http" "github.com/AlekSi/pointer" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "gopkg.in/reform.v1" @@ -32,29 +28,28 @@ import ( "github.com/percona/pmm/api/managementpb" "github.com/percona/pmm/managed/models" "github.com/percona/pmm/managed/services" + "github.com/percona/pmm/managed/utils/auth" ) // NodeService represents service for working with nodes. type NodeService struct { - db *reform.DB - akp apiKeyProvider - - l *logrus.Entry + db *reform.DB + ap authProvider + l *logrus.Entry } // NewNodeService creates NodeService instance. -func NewNodeService(db *reform.DB, akp apiKeyProvider) *NodeService { +func NewNodeService(db *reform.DB, ap authProvider) *NodeService { return &NodeService{ - db: db, - akp: akp, - l: logrus.WithField("component", "node"), + db: db, + ap: ap, + l: logrus.WithField("component", "node"), } } // Register do registration of the new node. func (s *NodeService) Register(ctx context.Context, req *managementpb.RegisterNodeRequest) (*managementpb.RegisterNodeResponse, error) { res := &managementpb.RegisterNodeResponse{} - e := s.db.InTransaction(func(tx *reform.TX) error { node, err := models.FindNodeByName(tx.Querier, req.NodeName) switch status.Code(err) { //nolint:exhaustive @@ -136,29 +131,43 @@ func (s *NodeService) Register(ctx context.Context, req *managementpb.RegisterNo return nil, e } - // get authorization from headers. - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - msg := "Couldn't create Admin API Key: cannot get headers from metadata" - s.l.Errorln(msg) - res.Warning = msg - return res, nil - } - authorizationHeaders := md.Get("Authorization") - if len(authorizationHeaders) == 0 { - return nil, status.Error(codes.Unauthenticated, "Authorization error.") - } - headers := make(http.Header) - headers.Set("Authorization", authorizationHeaders[0]) - if !s.akp.IsAPIKeyAuth(headers) { - apiKeyName := fmt.Sprintf("pmm-agent-%s-%d", req.NodeName, rand.Int63()) //nolint:gosec - _, res.Token, e = s.akp.CreateAdminAPIKey(ctx, apiKeyName) + authHeaders, _ := auth.GetHeadersFromContext(ctx) + token := auth.GetTokenFromHeaders(authHeaders) + if token != "" { + res.Token = token + } else { + _, res.Token, e = s.ap.CreateServiceAccount(ctx, req.NodeName, req.Reregister) if e != nil { - msg := fmt.Sprintf("Couldn't create Admin API Key: %s", e) - s.l.Errorln(msg) - res.Warning = msg + return nil, e } } return res, nil } + +// Unregister do unregistration of the node. +func (s *NodeService) Unregister(ctx context.Context, req *managementpb.UnregisterNodeRequest) (*managementpb.UnregisterNodeResponse, error) { + node, err := models.FindNodeByID(s.db.Querier, req.NodeId) + if err != nil { + return nil, err + } + + err = s.db.InTransaction(func(tx *reform.TX) error { + return models.RemoveNode(tx.Querier, req.NodeId, models.RemoveCascade) + }) + if err != nil { + return nil, err + } + + warning, err := s.ap.DeleteServiceAccount(ctx, node.NodeName, req.Force) + if err != nil { + s.l.WithError(err).Error("deleting service account") + return &managementpb.UnregisterNodeResponse{ + Warning: err.Error(), + }, nil + } + + return &managementpb.UnregisterNodeResponse{ + Warning: warning, + }, nil +} diff --git a/managed/services/management/node_test.go b/managed/services/management/node_test.go index d4f6d8472c..3241429229 100644 --- a/managed/services/management/node_test.go +++ b/managed/services/management/node_test.go @@ -21,7 +21,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -38,7 +37,10 @@ import ( ) func TestNodeService(t *testing.T) { - t.Run("Register", func(t *testing.T) { + getTestNodeName := func() string { + return "test-node" + } + t.Run("Register/Unregister", func(t *testing.T) { setup := func(t *testing.T) (ctx context.Context, s *NodeService, teardown func(t *testing.T)) { t.Helper() @@ -58,11 +60,17 @@ func TestNodeService(t *testing.T) { "Authorization": "Basic username:password", }) ctx = metadata.NewIncomingContext(ctx, md) - var apiKeyProvider mockApiKeyProvider - apiKeyProvider.Test(t) - apiKeyProvider.On("IsAPIKeyAuth", mock.Anything).Return(false) - apiKeyProvider.On("CreateAdminAPIKey", ctx, mock.AnythingOfType("string")).Return(int64(0), "test-token", nil) - s = NewNodeService(db, &apiKeyProvider) + + serviceAccountID := int(0) + nodeName := getTestNodeName() + reregister := false + force := true + + var authProvider mockAuthProvider + authProvider.Test(t) + authProvider.On("CreateServiceAccount", ctx, nodeName, reregister).Return(serviceAccountID, "test-token", nil) + authProvider.On("DeleteServiceAccount", ctx, nodeName, force).Return("", nil) + s = NewNodeService(db, &authProvider) return } @@ -73,14 +81,14 @@ func TestNodeService(t *testing.T) { res, err := s.Register(ctx, &managementpb.RegisterNodeRequest{ NodeType: inventorypb.NodeType_GENERIC_NODE, - NodeName: "node", + NodeName: "test-node", Address: "some.address.org", Region: "region", }) expected := &managementpb.RegisterNodeResponse{ GenericNode: &inventorypb.GenericNode{ NodeId: "/node_id/00000000-0000-4000-8000-000000000005", - NodeName: "node", + NodeName: "test-node", Address: "some.address.org", Region: "region", }, @@ -97,16 +105,25 @@ func TestNodeService(t *testing.T) { t.Run("Exist", func(t *testing.T) { res, err = s.Register(ctx, &managementpb.RegisterNodeRequest{ NodeType: inventorypb.NodeType_GENERIC_NODE, - NodeName: "node", + NodeName: "test-node", }) assert.Nil(t, res) - tests.AssertGRPCError(t, status.New(codes.AlreadyExists, `Node with name "node" already exists.`), err) + tests.AssertGRPCError(t, status.New(codes.AlreadyExists, `Node with name "test-node" already exists.`), err) }) t.Run("Reregister", func(t *testing.T) { + serviceAccountID := int(0) + nodeName := getTestNodeName() + reregister := true + + var authProvider mockAuthProvider + authProvider.Test(t) + authProvider.On("CreateServiceAccount", ctx, nodeName, reregister).Return(serviceAccountID, "test-token", nil) + s.ap = &authProvider + res, err = s.Register(ctx, &managementpb.RegisterNodeRequest{ NodeType: inventorypb.NodeType_GENERIC_NODE, - NodeName: "node", + NodeName: "test-node", Address: "some.address.org", Region: "region", Reregister: true, @@ -114,7 +131,7 @@ func TestNodeService(t *testing.T) { expected := &managementpb.RegisterNodeResponse{ GenericNode: &inventorypb.GenericNode{ NodeId: "/node_id/00000000-0000-4000-8000-000000000008", - NodeName: "node", + NodeName: "test-node", Address: "some.address.org", Region: "region", }, @@ -128,10 +145,20 @@ func TestNodeService(t *testing.T) { assert.Equal(t, expected, res) assert.NoError(t, err) }) + t.Run("Reregister-force", func(t *testing.T) { + serviceAccountID := int(0) + nodeName := "test-node-new" + reregister := true + + var authProvider mockAuthProvider + authProvider.Test(t) + authProvider.On("CreateServiceAccount", ctx, nodeName, reregister).Return(serviceAccountID, "test-token", nil) + s.ap = &authProvider + res, err = s.Register(ctx, &managementpb.RegisterNodeRequest{ NodeType: inventorypb.NodeType_GENERIC_NODE, - NodeName: "node-name-new", + NodeName: "test-node-new", Address: "some.address.org", Region: "region", Reregister: true, @@ -139,7 +166,7 @@ func TestNodeService(t *testing.T) { expected := &managementpb.RegisterNodeResponse{ GenericNode: &inventorypb.GenericNode{ NodeId: "/node_id/00000000-0000-4000-8000-00000000000b", - NodeName: "node-name-new", + NodeName: "test-node-new", Address: "some.address.org", Region: "region", }, @@ -153,6 +180,51 @@ func TestNodeService(t *testing.T) { assert.Equal(t, expected, res) assert.NoError(t, err) }) + + t.Run("Unregister", func(t *testing.T) { + serviceAccountID := int(0) + nodeName := getTestNodeName() + reregister := true + force := true + + var authProvider mockAuthProvider + authProvider.Test(t) + authProvider.On("CreateServiceAccount", ctx, nodeName, reregister).Return(serviceAccountID, "test-token", nil) + authProvider.On("DeleteServiceAccount", ctx, nodeName, force).Return("", nil) + s.ap = &authProvider + + resRegister, err := s.Register(ctx, &managementpb.RegisterNodeRequest{ + NodeType: inventorypb.NodeType_GENERIC_NODE, + NodeName: "test-node", + Address: "some.address.org", + Region: "region", + Reregister: true, + }) + assert.NoError(t, err) + + expected := &managementpb.RegisterNodeResponse{ + GenericNode: &inventorypb.GenericNode{ + NodeId: "/node_id/00000000-0000-4000-8000-00000000000e", + NodeName: "test-node", + Address: "some.address.org", + Region: "region", + }, + ContainerNode: (*inventorypb.ContainerNode)(nil), + PmmAgent: &inventorypb.PMMAgent{ + AgentId: "/agent_id/00000000-0000-4000-8000-00000000000f", + RunsOnNodeId: "/node_id/00000000-0000-4000-8000-00000000000e", + }, + Token: "test-token", + } + assert.Equal(t, expected, resRegister) + + res, err := s.Unregister(ctx, &managementpb.UnregisterNodeRequest{ + NodeId: resRegister.GenericNode.NodeId, + Force: true, + }) + assert.NoError(t, err) + assert.Equal(t, "", res.Warning) + }) }) }) } diff --git a/managed/utils/auth/token.go b/managed/utils/auth/token.go new file mode 100644 index 0000000000..43fd2b6635 --- /dev/null +++ b/managed/utils/auth/token.go @@ -0,0 +1,78 @@ +// Copyright (C) 2023 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 auth contains functions to work with auth tokens and headers. +package auth + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/gogo/status" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" +) + +// GetTokenFromHeaders returns authorization token if it is found in provided HTTP headers. +func GetTokenFromHeaders(authHeaders http.Header) string { + authHeader := authHeaders.Get("Authorization") + switch { + case strings.HasPrefix(authHeader, "Bearer"): + return strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer")) + case strings.HasPrefix(authHeader, "Basic"): + h := strings.TrimPrefix(authHeader, "Basic") + t, err := base64.StdEncoding.DecodeString(strings.TrimSpace(h)) + if err != nil { + logrus.Debugf("cannot decode basic authorization header: %s", err) + return "" + } + tk := string(t) + if strings.HasPrefix(tk, "api_key:") || strings.HasPrefix(tk, "service_token:") { + return strings.Split(tk, ":")[1] + } + } + + return "" +} + +// GetHeadersFromContext returns authorization headers if they are found in provided context. +func GetHeadersFromContext(ctx context.Context) (http.Header, error) { + headers, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, fmt.Errorf("cannot get headers from metadata") + } + + authorizationHeaders := headers.Get("Authorization") + cookieHeaders := headers.Get("grpcgateway-cookie") + if len(authorizationHeaders) == 0 && len(cookieHeaders) == 0 { + return nil, status.Error(codes.Unauthenticated, "Authorization error.") + } + + authHeaders := make(http.Header) + if len(authorizationHeaders) != 0 { + authHeaders.Add("Authorization", authorizationHeaders[0]) + } + if len(cookieHeaders) != 0 { + for _, header := range cookieHeaders { + authHeaders.Add("Cookie", header) + } + } + + return authHeaders, nil +}