Skip to content

Commit

Permalink
feat(agent): implements /cloud/metadata agent endpoint.
Browse files Browse the repository at this point in the history
This extends agent server with `/cloud/metadata` endpoint which returns
instance details such as `cloud_provider` and `instance_type`.
  • Loading branch information
VAveryanov8 committed Jan 10, 2025
1 parent c2b8fbd commit 969ee86
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 6 deletions.
4 changes: 3 additions & 1 deletion pkg/cmd/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/scylladb/scylla-manager/v3/swagger/gen/agent/models"
)

func newAgentHandler(c agent.Config, rclone http.Handler, logger log.Logger) *chi.Mux {
func newAgentHandler(c agent.Config, rclone http.Handler, cloudMeta http.HandlerFunc, logger log.Logger) *chi.Mux {
m := chi.NewMux()

m.Get("/node_info", newNodeInfoHandler(c).getNodeInfo)
Expand Down Expand Up @@ -49,6 +49,8 @@ func newAgentHandler(c agent.Config, rclone http.Handler, logger log.Logger) *ch
debug.FreeOSMemory()
})

m.Get("/cloud/metadata", cloudMeta)

// Rclone server
m.Mount("/rclone", http.StripPrefix("/agent/rclone", rclone))

Expand Down
62 changes: 62 additions & 0 deletions pkg/cmd/agent/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (C) 2024 ScyllaDB

package main

import (
"context"
"net/http"
"sync"

"github.com/go-chi/render"
"github.com/pkg/errors"
"github.com/scylladb/go-log"
"github.com/scylladb/scylla-manager/v3/pkg/cloudmeta"
"github.com/scylladb/scylla-manager/v3/swagger/gen/agent/models"
)

func newMetadataHandler(logger log.Logger) http.HandlerFunc {
var (
m sync.Mutex
loaded bool
metadata cloudmeta.InstanceMetadata
)

// Caches only successful result of GetInstanceMetadata.
lazyGetMetadata := func(ctx context.Context) (cloudmeta.InstanceMetadata, error) {
m.Lock()
defer m.Unlock()
if loaded {
return metadata, nil
}

metaSvc, err := cloudmeta.NewCloudMeta(logger)
if err != nil {
return cloudmeta.InstanceMetadata{}, errors.Wrap(err, "NewCloudMeta")
}

metadata, err = metaSvc.GetInstanceMetadata(ctx)
if err != nil {
return cloudmeta.InstanceMetadata{}, err
}

loaded = true
return metadata, nil
}

return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
instanceMeta, err := lazyGetMetadata(ctx)
if err != nil {
// Metadata may not be available for several reasons:
// 1. running on-premise 2. disabled 3. smth went wrong with metadata server.
// As we cannot distinguish between these cases, we can only log err.
logger.Error(ctx, "GetInstanceMetadata", "err", err)
render.Respond(w, r, models.InstanceMetadata{})
return
}
render.Respond(w, r, models.InstanceMetadata{
CloudProvider: string(instanceMeta.CloudProvider),
InstanceType: instanceMeta.InstanceType,
})
}
}
4 changes: 2 additions & 2 deletions pkg/cmd/agent/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

var unauthorizedErrorBody = json.RawMessage(`{"message":"unauthorized","code":401}`)

func newRouter(c agent.Config, metrics AgentMetrics, rclone http.Handler, logger log.Logger) http.Handler {
func newRouter(c agent.Config, metrics AgentMetrics, rclone http.Handler, cloudMeta http.HandlerFunc, logger log.Logger) http.Handler {
r := chi.NewRouter()

// Common middleware
Expand All @@ -34,7 +34,7 @@ func newRouter(c agent.Config, metrics AgentMetrics, rclone http.Handler, logger
auth.ValidateToken(c.AuthToken, time.Second, unauthorizedErrorBody),
)
// Agent specific endpoints
priv.Mount("/agent", newAgentHandler(c, rclone, logger.Named("agent")))
priv.Mount("/agent", newAgentHandler(c, rclone, cloudMeta, logger.Named("agent")))
// Scylla prometheus proxy
priv.Mount("/metrics", promProxy(c))
// Fallback to Scylla API proxy
Expand Down
43 changes: 41 additions & 2 deletions pkg/cmd/agent/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package main

import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
Expand All @@ -25,7 +26,7 @@ func TestRcloneRouting(t *testing.T) {
c := agent.Config{}
rclone := assertURLPath(t, "/foo")

h := newRouter(c, NewAgentMetrics(), rclone, log.NewDevelopment())
h := newRouter(c, NewAgentMetrics(), rclone, nil, log.NewDevelopment())
r := httptest.NewRequest(http.MethodGet, "/agent/rclone/foo", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
Expand All @@ -52,7 +53,7 @@ func TestProxyRouting(t *testing.T) {
},
}

h := newRouter(c, NewAgentMetrics(), nil, log.NewDevelopment())
h := newRouter(c, NewAgentMetrics(), nil, nil, log.NewDevelopment())

r := httptest.NewRequest(http.MethodGet, "/metrics", nil)
w := httptest.NewRecorder()
Expand All @@ -70,3 +71,41 @@ func TestProxyRouting(t *testing.T) {
t.Errorf("Response Code=%d expected %d", w.Code, http.StatusOK)
}
}

func TestCloudMetadataRouting(t *testing.T) {
c := agent.Config{}
rclone := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
cloudMeta := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"cloud_provider":"","instance_type":""}`))
})

h := newRouter(c, NewAgentMetrics(), rclone, cloudMeta, log.NewDevelopment())
r := httptest.NewRequest(http.MethodGet, "/agent/cloud/metadata", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Errorf("Response Code=%d expected %d", w.Code, http.StatusOK)
}

responseBody := map[string]string{}
if err := json.NewDecoder(w.Result().Body).Decode(&responseBody); err != nil {
t.Fatalf("decode body, unexpected err: %v", err)
}

cloudProvider, ok := responseBody["cloud_provider"]
if !ok {
t.Fatalf("`cloud_provider` field is expected")
}
if cloudProvider != "" {
t.Fatalf("expects `cloud_provider` to be empty, got %s", cloudProvider)
}

instanceType, ok := responseBody["instance_type"]
if !ok {
t.Fatalf("`instance_type` field is expected")
}
if instanceType != "" {
t.Fatalf("expects `instance_type` to be empty, got %s", instanceType)
}
}
3 changes: 2 additions & 1 deletion pkg/cmd/agent/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,11 @@ func (s *server) makeServers(ctx context.Context) error {
if err != nil {
return errors.Wrapf(err, "tls")
}
cloudMeta := newMetadataHandler(s.logger.Named("metadata"))
s.httpsServer = &http.Server{
Addr: s.config.HTTPS,
TLSConfig: tlsConfig,
Handler: newRouter(s.config, s.metrics, rcserver.New(), s.logger.Named("http")),
Handler: newRouter(s.config, s.metrics, rcserver.New(), cloudMeta, s.logger.Named("http")),
}
if s.config.Prometheus != "" {
s.prometheusServer = &http.Server{
Expand Down
22 changes: 22 additions & 0 deletions pkg/scyllaclient/client_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,25 @@ func (c *Client) FreeOSMemory(ctx context.Context, host string) error {
_, err := c.agentOps.FreeOSMemory(&p)
return errors.Wrap(err, "free OS memory")
}

// CloudMetadata returns instance metadata from agent node.
func (c *Client) CloudMetadata(ctx context.Context, host string) (InstanceMetadata, error) {
p := operations.MetadataParams{
Context: forceHost(ctx, host),
}

meta, err := c.agentOps.Metadata(&p)
if err != nil {
return InstanceMetadata{}, errors.Wrap(err, "cloud metadata")
}

payload := meta.GetPayload()
if payload == nil {
return InstanceMetadata{}, errors.New("payload is nil")
}

return InstanceMetadata{
CloudProvider: payload.CloudProvider,
InstanceType: payload.InstanceType,
}, nil
}
6 changes: 6 additions & 0 deletions pkg/scyllaclient/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,9 @@ func ViewBuildStatusOrder() []ViewBuildStatus {
func (s ViewBuildStatus) Index() int {
return slice.Index(ViewBuildStatusOrder(), s)
}

// InstanceMetadata defines node metadata needed for 1-to-1 restore.
type InstanceMetadata struct {
CloudProvider string
InstanceType string
}

0 comments on commit 969ee86

Please sign in to comment.