Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBG-4390: provide cluster UUID at top level endpoint #7345

Merged
merged 10 commits into from
Feb 6, 2025
Merged
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 @@ -1995,20 +1994,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 @@ -2018,27 +2005,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 @@ -2159,3 +2131,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)
}