From cd2ffcfff13e6a860e0c96243e34d0df51f68e04 Mon Sep 17 00:00:00 2001 From: Jonathan Gramain Date: Mon, 16 Sep 2024 16:52:36 -0700 Subject: [PATCH] BKTCLT-19 new bindings for raft session admin routes Add two new functions binding the admin routes: - AdminGetBucketSessionID (`GET /_/buckets/my-bucket/id`) - AdminGetSessionInfo/AdminGetAllSessionsInfo (`GET /_/raft_sessions`) --- go/admin_getbucketsessionid.go | 25 ++++++ go/admin_getbucketsessionid_test.go | 40 +++++++++ go/admin_getsessioninfo.go | 66 ++++++++++++++ go/admin_getsessioninfo_test.go | 132 ++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 go/admin_getbucketsessionid.go create mode 100644 go/admin_getbucketsessionid_test.go create mode 100644 go/admin_getsessioninfo.go create mode 100644 go/admin_getsessioninfo_test.go diff --git a/go/admin_getbucketsessionid.go b/go/admin_getbucketsessionid.go new file mode 100644 index 0000000..fd738e9 --- /dev/null +++ b/go/admin_getbucketsessionid.go @@ -0,0 +1,25 @@ +package bucketclient + +import ( + "context" + "fmt" + "strconv" +) + +// AdminGetBucketSessionID returns the raft session ID of the given bucket. +// Returns -1 and an error if the bucket doesn't exist, or if a request error occurs. +func (client *BucketClient) AdminGetBucketSessionID(ctx context.Context, bucketName string) (int, error) { + resource := fmt.Sprintf("/_/buckets/%s/id", bucketName) + responseBody, err := client.Request(ctx, "AdminGetBucketSessionID", "GET", resource) + if err != nil { + return -1, err + } + sessionId, err := strconv.ParseInt(string(responseBody), 10, 0) + if err != nil { + return -1, &BucketClientError{ + "AdminGetBucketSessionID", "GET", client.Endpoint, resource, 0, "", + fmt.Errorf("bucketd did not return a valid session ID in response body"), + } + } + return int(sessionId), nil +} diff --git a/go/admin_getbucketsessionid_test.go b/go/admin_getbucketsessionid_test.go new file mode 100644 index 0000000..24b154a --- /dev/null +++ b/go/admin_getbucketsessionid_test.go @@ -0,0 +1,40 @@ +package bucketclient_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/jarcoal/httpmock" + + "github.com/scality/bucketclient/go" +) + +var _ = Describe("AdminGetBucketSessionId()", Ordered, func() { + BeforeEach(func() { + httpmock.Reset() + }) + + var client *bucketclient.BucketClient + BeforeAll(func() { + httpmock.Activate() + client = bucketclient.New("http://localhost:9000") + }) + AfterAll(func() { + httpmock.DeactivateAndReset() + }) + It("return the raft session ID hosting a bucket", func(ctx SpecContext) { + httpmock.RegisterResponder( + "GET", "http://localhost:9000/_/buckets/my-bucket/id", + httpmock.NewStringResponder(200, "42"), + ) + Expect(client.AdminGetBucketSessionID(ctx, "my-bucket")).To(Equal(42)) + }) + It("return an error if the bucket doesn't exist", func(ctx SpecContext) { + httpmock.RegisterResponder( + "GET", "http://localhost:9000/_/buckets/nosuchbucket/id", + httpmock.NewStringResponder(404, ""), + ) + _, err := client.AdminGetBucketSessionID(ctx, "nosuchbucket") + Expect(err).To(MatchError(ContainSubstring("bucketd returned HTTP status 404"))) + }) +}) diff --git a/go/admin_getsessioninfo.go b/go/admin_getsessioninfo.go new file mode 100644 index 0000000..3a72901 --- /dev/null +++ b/go/admin_getsessioninfo.go @@ -0,0 +1,66 @@ +package bucketclient + +import ( + "context" + "encoding/json" + "fmt" +) + +type MemberInfo struct { + ID int `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Host string `json:"host"` + Port int `json:"port"` + AdminPort int `json:"adminPort"` + MDClusterId string `json:"mdClusterId"` +} + +type SessionInfo struct { + ID int `json:"id"` + RaftMembers []MemberInfo `json:"raftMembers"` + ConnectedToLeader bool `json:"connectedToLeader"` +} + +// AdminGetAllSessionsInfo returns raft session info for all Metadata +// raft sessions available on the S3C deployment. +func (client *BucketClient) AdminGetAllSessionsInfo(ctx context.Context) ([]SessionInfo, error) { + responseBody, err := client.Request(ctx, "AdminGetAllSessionsInfo", "GET", "/_/raft_sessions") + if err != nil { + return nil, err + } + + var parsedInfo []SessionInfo + jsonErr := json.Unmarshal(responseBody, &parsedInfo) + if jsonErr != nil { + return nil, ErrorMalformedResponse("AdminGetAllSessionsInfo", + "GET", client.Endpoint, "/_/raft_sessions", jsonErr) + } + return parsedInfo, nil +} + +// AdminGetSessionInfo returns raft session info for the given raft session ID. +// Returns nil and an error if the raft session doesn't exist, or if a request +// error occurs. +func (client *BucketClient) AdminGetSessionInfo(ctx context.Context, + sessionId int) (*SessionInfo, error) { + // When querying /_/raft_sessions/X/info, bucketd returns a + // status 500 if the raft session doesn't exist, which is hard + // to distinguish from a generic type of failure. For this + // reason, instead, we fetch the info for all raft sessions + // then lookup the one we want. + sessionsInfo, err := client.AdminGetAllSessionsInfo(ctx) + if err != nil { + return nil, err + } + for _, sessionInfo := range sessionsInfo { + if sessionInfo.ID == sessionId { + return &sessionInfo, nil + } + } + // raft session does not exist + return nil, &BucketClientError{ + "AdminGetSessionInfo", "GET", client.Endpoint, "/_/raft_sessions", 0, "", + fmt.Errorf("no such raft session: %d", sessionId), + } +} diff --git a/go/admin_getsessioninfo_test.go b/go/admin_getsessioninfo_test.go new file mode 100644 index 0000000..4b413d2 --- /dev/null +++ b/go/admin_getsessioninfo_test.go @@ -0,0 +1,132 @@ +package bucketclient_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/jarcoal/httpmock" + + "github.com/scality/bucketclient/go" +) + +var bucketdResponse = `[ + { + "id": 1, + "raftMembers": [ + { + "id": 10, + "name": "md1-cluster1", + "display_name": "127.0.0.1 (md1-cluster1)", + "host": "127.0.0.1", + "port": 4201, + "adminPort": 4251, + "mdClusterId": "1" + } + ], + "connectedToLeader": true + }, + { + "id": 2, + "raftMembers": [ + { + "id": 20, + "name": "md1-cluster1", + "display_name": "127.0.0.1 (md1-cluster1)", + "host": "127.0.0.1", + "port": 4202, + "adminPort": 4252, + "mdClusterId": "1" + } + ], + "connectedToLeader": false + } +] +` + +var _ = Describe("AdminGetSessionInfo()/AdminGetAllSessionsInfo()", Ordered, func() { + BeforeEach(func() { + httpmock.Reset() + }) + + var client *bucketclient.BucketClient + BeforeAll(func() { + httpmock.Activate() + client = bucketclient.New("http://localhost:9000") + }) + AfterAll(func() { + httpmock.DeactivateAndReset() + }) + + Describe("AdminGetAllSessionsInfo()", func() { + It("return info about all raft sessions", func(ctx SpecContext) { + httpmock.RegisterResponder( + "GET", "http://localhost:9000/_/raft_sessions", + httpmock.NewStringResponder(200, bucketdResponse), + ) + Expect(client.AdminGetAllSessionsInfo(ctx)).To(Equal([]bucketclient.SessionInfo{ + { + ID: 1, + RaftMembers: []bucketclient.MemberInfo{ + { + ID: 10, + Name: "md1-cluster1", + DisplayName: "127.0.0.1 (md1-cluster1)", + Host: "127.0.0.1", + Port: 4201, + AdminPort: 4251, + MDClusterId: "1", + }, + }, + ConnectedToLeader: true, + }, + { + ID: 2, + RaftMembers: []bucketclient.MemberInfo{ + { + ID: 20, + Name: "md1-cluster1", + DisplayName: "127.0.0.1 (md1-cluster1)", + Host: "127.0.0.1", + Port: 4202, + AdminPort: 4252, + MDClusterId: "1", + }, + }, + ConnectedToLeader: false, + }, + })) + }) + }) + + Describe("AdminGetSessionInfo()", func() { + It("return info about a particular raft session", func(ctx SpecContext) { + httpmock.RegisterResponder( + "GET", "http://localhost:9000/_/raft_sessions", + httpmock.NewStringResponder(200, bucketdResponse), + ) + Expect(client.AdminGetSessionInfo(ctx, 2)).To(Equal(&bucketclient.SessionInfo{ + ID: 2, + RaftMembers: []bucketclient.MemberInfo{ + { + ID: 20, + Name: "md1-cluster1", + DisplayName: "127.0.0.1 (md1-cluster1)", + Host: "127.0.0.1", + Port: 4202, + AdminPort: 4252, + MDClusterId: "1", + }, + }, + ConnectedToLeader: false, + })) + }) + It("return an error if the session doesn't exist", func(ctx SpecContext) { + httpmock.RegisterResponder( + "GET", "http://localhost:9000/_/raft_sessions", + httpmock.NewStringResponder(200, bucketdResponse), + ) + _, err := client.AdminGetSessionInfo(ctx, 3) + Expect(err).To(MatchError(ContainSubstring("no such raft session: 3"))) + }) + }) +})