Skip to content

Commit

Permalink
CBG-4392: [3.2.3 backport] provide cluster UUID at top level endpoint (
Browse files Browse the repository at this point in the history
…#7363)

* CBG-4390: provide cluster UUID at top level endpoint

* update to fix for non persistent config

* fix for rosmar

* remove pre paramatised test

* nit

* move mgtrequest into common function

* updates

* remove unused code

* fix failing test

* update to create separate struct of info for runtime only info
  • Loading branch information
gregns1 authored Feb 6, 2025
1 parent c28539f commit 3961d8c
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 54 deletions.
1 change: 0 additions & 1 deletion base/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ type BootstrapConnection interface {
// GetDocument retrieves the document with the specified key from the bucket's default collection.
// Returns exists=false if key is not found, returns error for any other error.
GetDocument(ctx context.Context, bucket, docID string, rv interface{}) (exists bool, err error)

// Close releases any long-lived connections
Close()
}
Expand Down
8 changes: 6 additions & 2 deletions base/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,19 @@ func GetServerUUID(ctx context.Context, store CouchbaseBucketStore) (uuid string
return "", err
}

return ParseClusterUUID(respBytes)
}

func ParseClusterUUID(respBytes []byte) (string, error) {
var responseJson struct {
ServerUUID string `json:"uuid"`
UUID string `json:"uuid"`
}

if err := JSONUnmarshal(respBytes, &responseJson); err != nil {
return "", err
}

return responseJson.ServerUUID, nil
return responseJson.UUID, nil
}

// Gets the metadata purge interval for the bucket. First checks for a bucket-specific value. If not
Expand Down
24 changes: 5 additions & 19 deletions base/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,31 +504,17 @@ func (b *GocbV2Bucket) MgmtRequest(ctx context.Context, method, uri, contentType
return nil, 0, err
}

req, err := http.NewRequest(method, mgmtEp+uri, body)
if err != nil {
return nil, 0, err
}

if contentType != "" {
req.Header.Add("Content-Type", contentType)
}

var username, password string
if b.Spec.Auth != nil {
username, password, _ := b.Spec.Auth.GetCredentials()
req.SetBasicAuth(username, password)
}
response, err := b.HttpClient(ctx).Do(req)
if err != nil {
return nil, response.StatusCode, err
username, password, _ = b.Spec.Auth.GetCredentials()
}
defer func() { _ = response.Body.Close() }()

respBytes, err := io.ReadAll(response.Body)
respBytes, statusCode, err := MgmtRequest(b.HttpClient(ctx), mgmtEp, method, uri, contentType, username, password, body)
if err != nil {
return nil, 0, err
return nil, statusCode, err
}

return respBytes, response.StatusCode, nil
return respBytes, statusCode, nil
}

// This prevents Sync Gateway from overflowing gocb's pipeline
Expand Down
30 changes: 30 additions & 0 deletions base/gocb_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net/http"
"os"
"time"

Expand Down Expand Up @@ -161,3 +163,31 @@ func getRootCAs(ctx context.Context, caCertPath string) (*x509.CertPool, error)
}
return rootCAs, nil
}

// MgmtRequest makes a request to the http couchbase management api. This function will read the entire contents of
// the response and return the output bytes, the status code, and an error.
func MgmtRequest(client *http.Client, mgmtEp, method, uri, contentType, username, password string, body io.Reader) ([]byte, int, error) {
req, err := http.NewRequest(method, mgmtEp+uri, body)
if err != nil {
return nil, 0, err
}

if contentType != "" {
req.Header.Add("Content-Type", contentType)
}

if username != "" {
req.SetBasicAuth(username, password)
}
response, err := client.Do(req)
if err != nil {
return nil, response.StatusCode, err
}
defer func() { _ = response.Body.Close() }()

respBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, 0, err
}
return respBytes, response.StatusCode, nil
}
15 changes: 14 additions & 1 deletion rest/admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,13 @@ func (h *handler) handleGetDbConfig() error {

type RunTimeServerConfigResponse struct {
*StartupConfig
Databases map[string]*DbConfig `json:"databases"`
RuntimeInformation `json:"runtime_information"`
Databases map[string]*DbConfig `json:"databases"`
}

// RuntimeInformation is a struct that holds runtime-only info in without interfering or being lost inside the actual StartupConfig properties.
type RuntimeInformation struct {
ClusterUUID string `json:"cluster_uuid"`
}

// Get admin config info
Expand Down Expand Up @@ -409,6 +415,13 @@ func (h *handler) handleGetConfig() error {
}
}

// grab cluster uuid for runtime config
clusterUUID, err := h.server.getClusterUUID(h.ctx())
if err != nil {
base.InfofCtx(h.ctx(), base.KeyConfig, "Could not determine cluster UUID: %s", err)
}
cfg.ClusterUUID = clusterUUID

// because loggers can be changed at runtime, we need to work backwards to get the config that would've created the actually running instances
cfg.Logging = *base.BuildLoggingConfigFromLoggers(h.server.Config.Logging)
cfg.Databases = databaseMap
Expand Down
61 changes: 61 additions & 0 deletions rest/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3240,3 +3240,64 @@ func TestRoleUpdatedAtField(t *testing.T) {
assert.Greater(t, newTime.UnixNano(), currTime.UnixNano())
assert.Equal(t, timeCreated.UnixNano(), newCreated.UnixNano())
}

func TestServerUUIDRuntimeServerConfig(t *testing.T) {
testCases := []struct {
name string
persistentConfig bool
}{
{
name: "Persistent config",
persistentConfig: true,
},
{
name: "non persistent config",
persistentConfig: false,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
rt := NewRestTester(t, &RestTesterConfig{PersistentConfig: testCase.persistentConfig})
defer rt.Close()

// create a db and test code pathway when we have db defined
if testCase.persistentConfig {
dbConfig := rt.NewDbConfig()
RequireStatus(t, rt.CreateDatabase("db", dbConfig), http.StatusCreated)
}

resp := rt.SendAdminRequest(http.MethodGet, "/_config?include_runtime=true", "")
RequireStatus(t, resp, http.StatusOK)

var clusterUUID string
config := RunTimeServerConfigResponse{}
err := base.JSONUnmarshal(resp.Body.Bytes(), &config)
require.NoError(t, err)
if base.TestUseCouchbaseServer() {
require.Len(t, config.ClusterUUID, 32)
} else {
require.Empty(t, config.ClusterUUID)
}
clusterUUID = config.ClusterUUID

// delete db and attempt to retrieve cluster UUID again to ensure we can still retrieve it
resp = rt.SendAdminRequest(http.MethodDelete, "/db/", "")
RequireStatus(t, resp, http.StatusOK)

resp = rt.SendAdminRequest(http.MethodGet, "/_config?include_runtime=true", "")
RequireStatus(t, resp, http.StatusOK)

config = RunTimeServerConfigResponse{}
err = base.JSONUnmarshal(resp.Body.Bytes(), &config)
require.NoError(t, err)
if base.TestUseCouchbaseServer() {
require.Len(t, config.ClusterUUID, 32)
} else {
require.Empty(t, config.ClusterUUID)
}
assert.Equal(t, clusterUUID, config.ClusterUUID)
})
}

}
63 changes: 32 additions & 31 deletions rest/server_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
Expand Down Expand Up @@ -2002,20 +2001,8 @@ func doHTTPAuthRequest(ctx context.Context, httpClient *http.Client, username, p
retryCount := 0

worker := func() (shouldRetry bool, err error, value interface{}) {
var httpResponse *http.Response

endpointIdx := retryCount % len(endpoints)
req, err := http.NewRequest(method, endpoints[endpointIdx]+path, bytes.NewBuffer(requestBody))
if err != nil {
return false, err, nil
}

req.SetBasicAuth(username, password)

httpResponse, err = httpClient.Do(req) // nolint:bodyclose // The body is closed outside of the worker loop
if err == nil {
return false, nil, httpResponse
}
responseBody, statusCode, err = base.MgmtRequest(httpClient, endpoints[endpointIdx], method, path, "", username, password, bytes.NewBuffer(requestBody))

if err, ok := err.(net.Error); ok && err.Timeout() {
retryCount++
Expand All @@ -2025,27 +2012,12 @@ func doHTTPAuthRequest(ctx context.Context, httpClient *http.Client, username, p
return false, err, nil
}

err, result := base.RetryLoop(ctx, "doHTTPAuthRequest", worker, base.CreateSleeperFunc(10, 100))
err, _ = base.RetryLoop(ctx, "doHTTPAuthRequest", worker, base.CreateSleeperFunc(10, 100))
if err != nil {
return 0, nil, err
}

httpResponse, ok := result.(*http.Response)
if !ok {
return 0, nil, fmt.Errorf("unexpected response type from doHTTPAuthRequest")
}

bodyString, err := io.ReadAll(httpResponse.Body)
if err != nil {
return 0, nil, err
}

err = httpResponse.Body.Close()
if err != nil {
return 0, nil, err
}

return httpResponse.StatusCode, bodyString, nil
return statusCode, responseBody, nil
}

// For test use
Expand Down Expand Up @@ -2166,3 +2138,32 @@ func getTotalMemory(ctx context.Context) uint64 {
}
return memory.Total
}

// getClusterUUID returns the cluster UUID. rosmar does not have a ClusterUUID, so this will return an empty cluster UUID and no error in this case.
func (sc *ServerContext) getClusterUUID(ctx context.Context) (string, error) {
allDbNames := sc.AllDatabaseNames()
// we can use db context to retrieve clusterUUID
if len(allDbNames) > 0 {
db, err := sc.GetDatabase(ctx, allDbNames[0])
if err == nil {
return db.ServerUUID, nil
}
}
// no cluster uuid for rosmar cluster
if base.ServerIsWalrus(sc.Config.Bootstrap.Server) {
return "", nil
}
// request server for cluster uuid
eps, client, err := sc.ObtainManagementEndpointsAndHTTPClient()
if err != nil {
return "", err
}
statusCode, output, err := doHTTPAuthRequest(ctx, client, sc.Config.Bootstrap.Username, sc.Config.Bootstrap.Password, http.MethodGet, "/pools", eps, nil)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("unable to get cluster UUID from server: %s", output)
}
return base.ParseClusterUUID(output)
}

0 comments on commit 3961d8c

Please sign in to comment.