From ab52b02def56a60c2dc3f33e011dd97c3eff4721 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Thu, 3 Mar 2022 12:13:32 +0100 Subject: [PATCH] http: add alloc service registration agent HTTP endpoint. --- command/agent/alloc_endpoint.go | 35 +++++++++ command/agent/alloc_endpoint_test.go | 113 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/command/agent/alloc_endpoint.go b/command/agent/alloc_endpoint.go index f6f724001..d5bfdf151 100644 --- a/command/agent/alloc_endpoint.go +++ b/command/agent/alloc_endpoint.go @@ -86,6 +86,8 @@ func (s *HTTPServer) AllocSpecificRequest(resp http.ResponseWriter, req *http.Re switch tokens[1] { case "stop": return s.allocStop(allocID, resp, req) + case "services": + return s.allocServiceRegistrations(resp, req, allocID) } return nil, CodedError(404, resourceNotFoundErr) @@ -167,6 +169,39 @@ func (s *HTTPServer) allocStop(allocID string, resp http.ResponseWriter, req *ht return &out, nil } +// allocServiceRegistrations returns a list of all service registrations +// assigned to the job identifier. It is callable via the +// /v1/allocation/:alloc_id/services HTTP API and uses the +// structs.AllocServiceRegistrationsRPCMethod RPC method. +func (s *HTTPServer) allocServiceRegistrations( + resp http.ResponseWriter, req *http.Request, allocID string) (interface{}, error) { + + // The endpoint only supports GET requests. + if req.Method != http.MethodGet { + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } + + // Set up the request args and parse this to ensure the query options are + // set. + args := structs.AllocServiceRegistrationsRequest{AllocID: allocID} + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + // Perform the RPC request. + var reply structs.AllocServiceRegistrationsResponse + if err := s.agent.RPC(structs.AllocServiceRegistrationsRPCMethod, &args, &reply); err != nil { + return nil, err + } + + setMeta(resp, &reply.QueryMeta) + + if reply.Services == nil { + return nil, CodedError(http.StatusNotFound, allocNotFoundErr) + } + return reply.Services, nil +} + func (s *HTTPServer) ClientAllocRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/client/allocation/") diff --git a/command/agent/alloc_endpoint_test.go b/command/agent/alloc_endpoint_test.go index ce1d994bc..8d8b293f4 100644 --- a/command/agent/alloc_endpoint_test.go +++ b/command/agent/alloc_endpoint_test.go @@ -433,6 +433,119 @@ func TestHTTP_AllocStop(t *testing.T) { }) } +func TestHTTP_allocServiceRegistrations(t *testing.T) { + t.Parallel() + + testCases := []struct { + testFn func(srv *TestAgent) + name string + }{ + { + testFn: func(s *TestAgent) { + + // Grab the state, so we can manipulate it and test against it. + testState := s.Agent.server.State() + + // Generate an alloc and upsert this. + alloc := mock.Alloc() + require.NoError(t, testState.UpsertAllocs( + structs.MsgTypeTestSetup, 10, []*structs.Allocation{alloc})) + + // Generate a service registration, assigned the allocID to the + // mocked allocation ID, and upsert this. + serviceReg := mock.ServiceRegistrations()[0] + serviceReg.AllocID = alloc.ID + require.NoError(t, testState.UpsertServiceRegistrations( + structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{serviceReg})) + + // Build the HTTP request. + path := fmt.Sprintf("/v1/allocation/%s/services", alloc.ID) + req, err := http.NewRequest(http.MethodGet, path, nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := s.Server.AllocSpecificRequest(respW, req) + require.NoError(t, err) + + // Check the response. + require.Equal(t, "20", respW.Header().Get("X-Nomad-Index")) + require.ElementsMatch(t, []*structs.ServiceRegistration{serviceReg}, + obj.([]*structs.ServiceRegistration)) + }, + name: "alloc has registrations", + }, + { + testFn: func(s *TestAgent) { + + // Grab the state, so we can manipulate it and test against it. + testState := s.Agent.server.State() + + // Generate an alloc and upsert this. + alloc := mock.Alloc() + require.NoError(t, testState.UpsertAllocs( + structs.MsgTypeTestSetup, 10, []*structs.Allocation{alloc})) + + // Build the HTTP request. + path := fmt.Sprintf("/v1/allocation/%s/services", alloc.ID) + req, err := http.NewRequest(http.MethodGet, path, nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := s.Server.AllocSpecificRequest(respW, req) + require.NoError(t, err) + + // Check the response. + require.Equal(t, "1", respW.Header().Get("X-Nomad-Index")) + require.ElementsMatch(t, []*structs.ServiceRegistration{}, + obj.([]*structs.ServiceRegistration)) + }, + name: "alloc without registrations", + }, + { + testFn: func(s *TestAgent) { + + // Build the HTTP request. + path := fmt.Sprintf("/v1/allocation/%s/services", uuid.Generate()) + req, err := http.NewRequest(http.MethodGet, path, nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := s.Server.AllocSpecificRequest(respW, req) + require.Error(t, err) + require.Contains(t, err.Error(), "allocation not found") + require.Nil(t, obj) + }, + name: "alloc not found", + }, + { + testFn: func(s *TestAgent) { + + // Build the HTTP request. + path := fmt.Sprintf("/v1/allocation/%s/services", uuid.Generate()) + req, err := http.NewRequest(http.MethodHead, path, nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := s.Server.AllocSpecificRequest(respW, req) + require.Error(t, err) + require.Contains(t, err.Error(), "Invalid method") + require.Nil(t, obj) + }, + name: "alloc not found", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + httpTest(t, nil, tc.testFn) + }) + } +} + func TestHTTP_AllocStats(t *testing.T) { t.Parallel() require := require.New(t)