Skip to content

Commit

Permalink
BKTCLT-19 new bindings for raft session admin routes
Browse files Browse the repository at this point in the history
Add two new functions binding the admin routes:

- AdminGetBucketSessionID (`GET /_/buckets/my-bucket/id`)

- AdminGetSessionInfo/AdminGetAllSessionsInfo (`GET /_/raft_sessions`)
  • Loading branch information
jonathan-gramain committed Sep 18, 2024
1 parent 962ceb4 commit 4f32cbf
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 0 deletions.
25 changes: 25 additions & 0 deletions go/admin_getbucketsessionid.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions go/admin_getbucketsessionid_test.go
Original file line number Diff line number Diff line change
@@ -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")))
})
})
67 changes: 67 additions & 0 deletions go/admin_getsessioninfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 a 404 status as if coming from bucketd
return nil, &BucketClientError{
"AdminGetSessionInfo", "GET", client.Endpoint, "/_/raft_sessions",
404, "RaftSessionNotFound",
fmt.Errorf("no such raft session: %d", sessionId),
}
}
135 changes: 135 additions & 0 deletions go/admin_getsessioninfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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)
bcErr, ok := err.(*bucketclient.BucketClientError)
Expect(ok).To(BeTrue())
Expect(bcErr.StatusCode).To(Equal(404))
Expect(bcErr.ErrorType).To(Equal("RaftSessionNotFound"))
})
})
})

0 comments on commit 4f32cbf

Please sign in to comment.