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

add HTTP endpoint to reload agent #312

Merged
merged 3 commits into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"os/signal"
"syscall"

metrics "github.com/armon/go-metrics"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/agent/config"
agentServer "github.com/hashicorp/nomad-autoscaler/agent/http"
"github.com/hashicorp/nomad-autoscaler/plugins/manager"
"github.com/hashicorp/nomad-autoscaler/policy"
filePolicy "github.com/hashicorp/nomad-autoscaler/policy/file"
Expand All @@ -26,7 +26,7 @@ type Agent struct {
nomadClient *api.Client
pluginManager *manager.PluginManager
policyManager *policy.Manager
httpServer *agentServer.Server
inMemSink *metrics.InmemSink
evalBroker *policyeval.Broker
}

Expand Down Expand Up @@ -59,15 +59,7 @@ func (a *Agent) Run() error {
if err != nil {
return fmt.Errorf("failed to setup telemetry: %v", err)
}

// Setup and start the HTTP server.
httpServer, err := agentServer.NewHTTPServer(a.config.HTTP, a.logger, inMem)
if err != nil {
return fmt.Errorf("failed to setup HTTP getHealth server: %v", err)
}

a.httpServer = httpServer
go a.httpServer.Start()
a.inMemSink = inMem

policyEvalCh := a.setupPolicyManager()
go a.policyManager.Run(ctx, policyEvalCh)
Expand Down Expand Up @@ -144,11 +136,6 @@ func (a *Agent) setupPolicyManager() chan *sdk.ScalingEvaluation {
}

func (a *Agent) stop() {
// Stop the health server.
if a.httpServer != nil {
a.httpServer.Stop()
}

// Kill all the plugins.
if a.pluginManager != nil {
a.pluginManager.KillPlugins()
Expand Down Expand Up @@ -214,7 +201,8 @@ func (a *Agent) generateNomadClient() error {

// reload triggers the reload of sub-routines based on the operator sending a
// SIGHUP signal to the agent.
func (a Agent) reload() {
func (a *Agent) reload() {
a.logger.Debug("reloading policy sources")
a.policyManager.ReloadSources()
}

Expand Down
25 changes: 25 additions & 0 deletions agent/http/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package http

import (
"net/http"
"strings"
)

// agentSpecificRequest handles the requests for the `/v1/agent/` endpoint and sub-paths.
func (s *Server) agentSpecificRequest(w http.ResponseWriter, r *http.Request) (interface{}, error) {
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
path := strings.TrimPrefix(r.URL.Path, "/v1/agent")
switch {
case strings.HasSuffix(path, "/reload"):
return s.agentReload(w, r)
default:
return nil, newCodedError(http.StatusNotFound, "")
}
}

func (s *Server) agentReload(w http.ResponseWriter, r *http.Request) (interface{}, error) {
if r.Method != http.MethodPost && r.Method != http.MethodPut {
return nil, newCodedError(http.StatusMethodNotAllowed, errInvalidMethod)
}

return s.agent.ReloadAgent(w, r)
}
41 changes: 41 additions & 0 deletions agent/http/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package http

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestServer_agentReload(t *testing.T) {
testCases := []struct {
inputReq *http.Request
expectedRespCode int
name string
}{
{
inputReq: httptest.NewRequest("PUT", "/v1/agent/reload", nil),
expectedRespCode: 200,
name: "successfully reload",
},
{
inputReq: httptest.NewRequest("GET", "/v1/agent/reload", nil),
expectedRespCode: 405,
name: "incorrect request method",
},
}

srv, stopSrv := TestServer(t)
defer stopSrv()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)

w := httptest.NewRecorder()
srv.mux.ServeHTTP(w, tc.inputReq)
assert.Equal(tc.expectedRespCode, w.Code)
})
}
}
7 changes: 2 additions & 5 deletions agent/http/health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"sync/atomic"
"testing"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/agent/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -43,9 +41,8 @@ func TestServer_getHealth(t *testing.T) {
}

// Create our HTTP server.
srv, err := NewHTTPServer(&config.HTTP{BindAddress: "127.0.0.1", BindPort: 8080}, hclog.NewNullLogger(), nil)
assert.Nil(t, err)
defer srv.ln.Close()
srv, stopSrv := TestServer(t)
defer stopSrv()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion agent/http/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (s *Server) getMetrics(w http.ResponseWriter, r *http.Request) (interface{}
s.getPrometheusMetrics().ServeHTTP(w, r)
return nil, nil
}
return s.inMemSink.DisplayMetrics(w, r)
return s.agent.DisplayMetrics(w, r)
}

// getPrometheusMetrics is the getMetrics handler when the caller wishes to
Expand Down
13 changes: 2 additions & 11 deletions agent/http/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

metrics "github.com/armon/go-metrics"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/agent/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -44,14 +40,9 @@ func TestServer_getMetrics(t *testing.T) {
},
}

// Create a simple in-memory sink to use.
inm := metrics.NewInmemSink(10*time.Second, time.Minute)
metrics.DefaultInmemSignal(inm)

// Create our HTTP server.
srv, err := NewHTTPServer(&config.HTTP{BindAddress: "127.0.0.1", BindPort: 8080}, hclog.NewNullLogger(), inm)
assert.Nil(t, err)
defer srv.ln.Close()
srv, stopSrv := TestServer(t)
defer stopSrv()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
30 changes: 22 additions & 8 deletions agent/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"sync/atomic"
"time"

metrics "github.com/armon/go-metrics"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/nomad-autoscaler/agent/config"
Expand All @@ -24,13 +23,27 @@ const (
// to register the metrics server endpoint.
metricsRoutePattern = "/v1/metrics"

// agentRoutePattern is the Autoscaler HTTP router pattern which is used to
// register endpoints related to the agent.
agentRoutePattern = "/v1/agent/"

// healthAliveness is used to define the health of the Autoscaler agent. It
// currently can only be in two states; ready or unavailable and depends
// entirely on whether the server is serving or not.
healthAlivenessReady = iota
healthAlivenessUnavailable
)

// AgentHTTP is the interface that defines the HTTP handlers that an Agent
// must implement in order to be accessible through the HTTP API.
type AgentHTTP interface {
// DisplayMetrics returns a summary of metrics collected by the agent.
DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error)

// ReloadAgent triggers the agent to reload policies and configuration.
ReloadAgent(resp http.ResponseWriter, req *http.Request) (interface{}, error)
}

type Server struct {
log hclog.Logger
ln net.Listener
Expand All @@ -42,23 +55,24 @@ type Server struct {
// const declarations.
aliveness int32

// inMemSink is our in-memory telemetry sink used to server metrics
// endpoint requests.
inMemSink *metrics.InmemSink
// agent is the reference to an object that implements the AgentHTTP
// interface to handle agent requests.
agent AgentHTTP
}

// NewHTTPServer creates a new agent HTTP server.
func NewHTTPServer(cfg *config.HTTP, log hclog.Logger, inmSink *metrics.InmemSink) (*Server, error) {
func NewHTTPServer(cfg *config.HTTP, log hclog.Logger, agent AgentHTTP) (*Server, error) {

srv := &Server{
inMemSink: inmSink,
log: log.Named("http_server"),
mux: http.NewServeMux(),
log: log.Named("http_server"),
mux: http.NewServeMux(),
agent: agent,
}

// Setup our handlers.
srv.mux.HandleFunc(healthRoutePattern, srv.wrap(srv.getHealth))
srv.mux.HandleFunc(metricsRoutePattern, srv.wrap(srv.getMetrics))
srv.mux.HandleFunc(agentRoutePattern, srv.wrap(srv.agentSpecificRequest))

// Configure the HTTP server to the most basic level.
srv.srv = &http.Server{
Expand Down
25 changes: 25 additions & 0 deletions agent/http/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package http

import (
"testing"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/agent"
"github.com/hashicorp/nomad-autoscaler/agent/config"
)

func TestServer(t *testing.T) (*Server, func()) {
cfg := &config.HTTP{
BindAddress: "127.0.0.1",
BindPort: 0, // Use next available port.
}

s, err := NewHTTPServer(cfg, hclog.NewNullLogger(), &agent.MockAgentHTTP{})
if err != nil {
t.Fatalf("failed to start test server: %v", err)
}

return s, func() {
s.Stop()
}
}
14 changes: 14 additions & 0 deletions agent/http_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package agent

import "net/http"

// The methods in this file implement in the http.AgentHTTP interface.
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved

func (a *Agent) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return a.inMemSink.DisplayMetrics(resp, req)
}

func (a *Agent) ReloadAgent(_ http.ResponseWriter, _ *http.Request) (interface{}, error) {
a.reload()
return nil, nil
}
22 changes: 22 additions & 0 deletions agent/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package agent

import (
"net/http"

metrics "github.com/armon/go-metrics"
)

type MockAgentHTTP struct{}

func (m *MockAgentHTTP) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return metrics.MetricsSummary{
Timestamp: "2020-11-17 00:17:50 +0000 UTC",
Counters: []metrics.SampledValue{},
Gauges: []metrics.GaugeValue{},
Points: []metrics.PointValue{},
Samples: []metrics.SampledValue{},
}, nil
}
func (m *MockAgentHTTP) ReloadAgent(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return nil, nil
}
20 changes: 17 additions & 3 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad-autoscaler/agent"
"github.com/hashicorp/nomad-autoscaler/agent/config"
agentHTTP "github.com/hashicorp/nomad-autoscaler/agent/http"
flaghelper "github.com/hashicorp/nomad-autoscaler/sdk/helper/flag"
)

type AgentCommand struct {
args []string

agent *agent.Agent
httpServer *agentHTTP.Server
}

// Help should return long-form help text that includes the command-line
Expand Down Expand Up @@ -226,9 +230,19 @@ func (c *AgentCommand) Run(args []string) int {
JSONFormat: parsedConfig.LogJson,
})

// create and run agent
a := agent.NewAgent(parsedConfig, logger)
if err := a.Run(); err != nil {
// create and run agent and HTTP server
c.agent = agent.NewAgent(parsedConfig, logger)
httpServer, err := agentHTTP.NewHTTPServer(parsedConfig.HTTP, logger, c.agent)
if err != nil {
logger.Error("failed to setup HTTP getHealth server", "error", err)
return 1
}

c.httpServer = httpServer
go c.httpServer.Start()
defer c.httpServer.Stop()

if err := c.agent.Run(); err != nil {
logger.Error("failed to start agent", "error", err)
return 1
}
Expand Down