Skip to content

Commit

Permalink
add v1 versioning to introspection and additional logging
Browse files Browse the repository at this point in the history
  • Loading branch information
willmyrs committed Jan 29, 2025
1 parent a881dd9 commit d790772
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 311 deletions.
185 changes: 0 additions & 185 deletions ecs-agent/introspection/handlers.go
Original file line number Diff line number Diff line change
@@ -1,197 +1,12 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package introspection

import (
"errors"
"fmt"
"net/http"

"github.com/aws/amazon-ecs-agent/ecs-agent/metrics"
tmdsutils "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
)

const (
dockerIDQueryField = "dockerid"
taskARNQueryField = "taskarn"
dockerShortIDLen = 12
requestTypeAgent = "introspection/agent"
requestTypeTasks = "introspection/tasks"
requestTypeLicense = "introspection/license"
licensePath = "/license"
agentMetadataPath = "/v1/metadata"
tasksMetadataPath = "/v1/tasks"
)

// licenseHandler creates response for '/license' API.
func licenseHandler(agentState AgentState, metricsFactory metrics.EntryFactory) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
text, err := agentState.GetLicenseText()
if err != nil {
metricsFactory.New(metrics.IntrospectionInternalServerError).Done(err)
tmdsutils.WriteStringToResponse(w, http.StatusInternalServerError, "", requestTypeLicense)
} else {
tmdsutils.WriteStringToResponse(w, http.StatusOK, text, requestTypeLicense)
}
}
}

// getHTTPErrorCode returns an appropriate HTTP response status code and body for the error.
func getHTTPErrorCode(err error) (int, string) {
// There was something wrong with the request
var errBadRequest *ErrorBadRequest
if errors.As(err, &errBadRequest) {
return errBadRequest.StatusCode(), errBadRequest.MetricName()
}

// The requested object was not found
var errNotFound *ErrorNotFound
if errors.As(err, &errNotFound) {
return errNotFound.StatusCode(), errNotFound.MetricName()
}

// There was an error finding the object, but we have some info to return
var errFetchFailure *ErrorFetchFailure
if errors.As(err, &errFetchFailure) {
return errFetchFailure.StatusCode(), errFetchFailure.MetricName()
}

// Some unkown error has occurred
return http.StatusInternalServerError, metrics.IntrospectionInternalServerError
}

// agentMetadataHandler returns the HTTP handler function for handling agent metadata requests.
func agentMetadataHandler(
agentState AgentState,
metricsFactory metrics.EntryFactory,
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
agentMetadata, err := agentState.GetAgentMetadata()
if err != nil {
responseCode, metricName := getHTTPErrorCode(err)
metricsFactory.New(metricName).Done(err)
tmdsutils.WriteJSONResponse(w, responseCode, AgentMetadataResponse{}, requestTypeAgent)
return
}
tmdsutils.WriteJSONResponse(w, http.StatusOK, agentMetadata, requestTypeAgent)
}
}

// tasksMetadataHandler returns the HTTP handler function for handling tasks metadata requests.
func tasksMetadataHandler(
agentState AgentState,
metricsFactory metrics.EntryFactory,
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
dockerID, dockerIDExists := tmdsutils.ValueFromRequest(r, dockerIDQueryField)
taskArn, taskARNExists := tmdsutils.ValueFromRequest(r, taskARNQueryField)
if dockerIDExists && taskARNExists {
errorMsg := fmt.Sprintf("Request contains both %s and %s. Expect at most one of these.", dockerIDQueryField, taskARNQueryField)
metricsFactory.New(metrics.IntrospectionBadRequest).Done(errors.New(errorMsg))
tmdsutils.WriteJSONResponse(w, http.StatusBadRequest, TaskResponse{}, requestTypeTasks)
return
}
if dockerIDExists {
// Find the task that has a container with a matching docker ID.
if len(dockerID) > dockerShortIDLen {
getTaskByID(agentState, metricsFactory, dockerID, w)
return
} else {
getTaskByShortID(agentState, metricsFactory, dockerID, w)
return
}
} else if taskARNExists {
// Find the task with a matching Arn.
getTaskByArn(agentState, metricsFactory, taskArn, w)
return
} else {
// Return all tasks.
getTasksMetadata(agentState, metricsFactory, w)
}
}
}

// getTasksMetadata writes a list of metadata for all tasks to the response
func getTasksMetadata(
agentState AgentState,
metricsFactory metrics.EntryFactory,
w http.ResponseWriter,
) {
tasksMetadata, err := agentState.GetTasksMetadata()
if err != nil {
responseCode, metricName := getHTTPErrorCode(err)
metricsFactory.New(metricName).Done(err)
tmdsutils.WriteJSONResponse(w, responseCode, TasksResponse{}, requestTypeTasks)
return
}
tmdsutils.WriteJSONResponse(w, http.StatusOK, tasksMetadata, requestTypeTasks)
}

// getTaskByArn writes metadata for the corresponding task to the response, or an
// error status if the agent cannot return the metadata
func getTaskByArn(
agentState AgentState,
metricsFactory metrics.EntryFactory,
taskArn string,
w http.ResponseWriter,
) {
taskMetadata, err := agentState.GetTaskMetadataByArn(taskArn)
if err != nil {
responseCode, metricName := getHTTPErrorCode(err)
metricsFactory.New(metricName).Done(err)
tmdsutils.WriteJSONResponse(w, responseCode, TaskResponse{}, requestTypeTasks)
return
}
tmdsutils.WriteJSONResponse(w, http.StatusOK, taskMetadata, requestTypeTasks)
}

// getTaskByID writes metadata for the corresponding task to the response, or an
// error status if the agent cannot return the metadata
func getTaskByID(
agentState AgentState,
metricsFactory metrics.EntryFactory,
dockerID string,
w http.ResponseWriter,
) {
taskMetadata, err := agentState.GetTaskMetadataByID(dockerID)
if err != nil {
responseCode, metricName := getHTTPErrorCode(err)
metricsFactory.New(metricName).Done(err)
tmdsutils.WriteJSONResponse(w, responseCode, TaskResponse{}, requestTypeTasks)
return
}
tmdsutils.WriteJSONResponse(w, http.StatusOK, taskMetadata, requestTypeTasks)
}

// getTaskByShortID writes metadata for the corresponding task to the response, or an
// error status if the agent cannot return the metadata
func getTaskByShortID(
agentState AgentState,
metricsFactory metrics.EntryFactory,
shortDockerID string,
w http.ResponseWriter,
) {
taskMetadata, err := agentState.GetTaskMetadataByShortID(shortDockerID)
if err != nil {
responseCode, metricName := getHTTPErrorCode(err)
metricsFactory.New(metricName).Done(err)
tmdsutils.WriteJSONResponse(w, responseCode, TaskResponse{}, requestTypeTasks)
return
}
tmdsutils.WriteJSONResponse(w, http.StatusOK, taskMetadata, requestTypeTasks)
}

// panicHandler handler will gracefully close the connection if a panic occurs, returning
// an internal server error to the client.
func panicHandler(next http.Handler, metricsFactory metrics.EntryFactory) http.Handler {
Expand Down
16 changes: 9 additions & 7 deletions ecs-agent/introspection/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"strconv"
"time"

v1 "github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1"
"github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1/handlers"
"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
"github.com/aws/amazon-ecs-agent/ecs-agent/metrics"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/logging"
Expand Down Expand Up @@ -85,19 +87,19 @@ func WithWriteTimeout(writeTimeout time.Duration) ConfigOpt {
}

// Create a new HTTP Introspection Server
func NewServer(agentState AgentState, metricsFactory metrics.EntryFactory, options ...ConfigOpt) (*http.Server, error) {
func NewServer(agentState v1.AgentState, metricsFactory metrics.EntryFactory, options ...ConfigOpt) (*http.Server, error) {
config := new(Config)
for _, opt := range options {
opt(config)
}
return setup(agentState, metricsFactory, config)
}
func v1HandlersSetup(serverMux *http.ServeMux,
agentState AgentState,
agentState v1.AgentState,
metricsFactory metrics.EntryFactory) {
serverMux.HandleFunc(agentMetadataPath, agentMetadataHandler(agentState, metricsFactory))
serverMux.HandleFunc(tasksMetadataPath, tasksMetadataHandler(agentState, metricsFactory))
serverMux.HandleFunc(licensePath, licenseHandler(agentState, metricsFactory))
serverMux.HandleFunc(handlers.V1AgentMetadataPath, handlers.AgentMetadataHandler(agentState, metricsFactory))
serverMux.HandleFunc(handlers.V1TasksMetadataPath, handlers.TasksMetadataHandler(agentState, metricsFactory))
serverMux.HandleFunc(handlers.V1LicensePath, handlers.LicenseHandler(agentState, metricsFactory))
}
func pprofHandlerSetup(serverMux *http.ServeMux) {
serverMux.HandleFunc(pprofBasePath, pprofIndexHandler)
Expand All @@ -107,7 +109,7 @@ func pprofHandlerSetup(serverMux *http.ServeMux) {
serverMux.HandleFunc(pprofTracePath, pprofTraceHandler)
}
func setup(
agentState AgentState,
agentState v1.AgentState,
metricsFactory metrics.EntryFactory,
config *Config,
) (*http.Server, error) {
Expand All @@ -118,7 +120,7 @@ func setup(
return nil, errors.New("metrics factory cannot be nil")
}

paths := []string{agentMetadataPath, tasksMetadataPath, licensePath}
paths := []string{handlers.V1AgentMetadataPath, handlers.V1TasksMetadataPath, handlers.V1LicensePath}

if config.enableRuntimeStats {
paths = append(paths, pprofBasePath, pprofCMDLinePath, pprofProfilePath, pprofSymbolPath, pprofTracePath)
Expand Down
27 changes: 15 additions & 12 deletions ecs-agent/introspection/server_integ_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"net/http/httptest"
"testing"

v1 "github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1"
"github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1/handlers"
mock_v1 "github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1/mocks"
"github.com/aws/amazon-ecs-agent/ecs-agent/metrics"
mock_metrics "github.com/aws/amazon-ecs-agent/ecs-agent/metrics/mocks"
"github.com/aws/aws-sdk-go/aws"
Expand All @@ -30,13 +33,13 @@ import (
"github.com/stretchr/testify/require"
)

func testSetup(t *testing.T) (*gomock.Controller, *MockAgentState,
func testSetup(t *testing.T) (*gomock.Controller, *mock_v1.MockAgentState,
*mock_metrics.MockEntryFactory, *http.Server,
) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

agentState := NewMockAgentState(ctrl)
agentState := mock_v1.NewMockAgentState(ctrl)
metricsFactory := mock_metrics.NewMockEntryFactory(ctrl)

server, err := NewServer(agentState, metricsFactory)
Expand All @@ -61,7 +64,7 @@ func TestRoutes(t *testing.T) {
t.Run("agent metadata - happy case", func(t *testing.T) {
_, mockAgentState, _, server := testSetup(t)

testAgentMetadata := &AgentMetadataResponse{
testAgentMetadata := &v1.AgentMetadataResponse{
Cluster: "cluster",
ContainerInstanceArn: aws.String("some/arn"),
Version: "1.0.0",
Expand All @@ -71,7 +74,7 @@ func TestRoutes(t *testing.T) {
GetAgentMetadata().
Return(testAgentMetadata, nil)

recorder := performMockRequest(t, server, agentMetadataPath)
recorder := performMockRequest(t, server, handlers.V1AgentMetadataPath)

testAgentMetadataJSON, _ := json.Marshal(testAgentMetadata)

Expand All @@ -82,7 +85,7 @@ func TestRoutes(t *testing.T) {
t.Run("agent metadata - fetch failed", func(t *testing.T) {
mockCtrl, mockAgentState, mockMetricsFactory, server := testSetup(t)

testErr := NewErrorFetchFailure("some error")
testErr := v1.NewErrorFetchFailure("some error")

mockAgentState.EXPECT().
GetAgentMetadata().
Expand All @@ -93,9 +96,9 @@ func TestRoutes(t *testing.T) {
mockMetricsFactory.EXPECT().
New(metrics.IntrospectionFetchFailure).Return(mockEntry)

recorder := performMockRequest(t, server, agentMetadataPath)
recorder := performMockRequest(t, server, handlers.V1AgentMetadataPath)

emptyMetadataResponse, _ := json.Marshal(AgentMetadataResponse{})
emptyMetadataResponse, _ := json.Marshal(v1.AgentMetadataResponse{})

assert.Equal(t, http.StatusInternalServerError, recorder.Code)
assert.Equal(t, string(emptyMetadataResponse), recorder.Body.String())
Expand All @@ -110,7 +113,7 @@ func TestRoutes(t *testing.T) {
GetLicenseText().
Return(licenseText, nil)

recorder := performMockRequest(t, server, licensePath)
recorder := performMockRequest(t, server, handlers.V1LicensePath)

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, string(licenseText), recorder.Body.String())
Expand All @@ -130,7 +133,7 @@ func TestRoutes(t *testing.T) {
mockMetricsFactory.EXPECT().
New(metrics.IntrospectionInternalServerError).Return(mockEntry)

recorder := performMockRequest(t, server, licensePath)
recorder := performMockRequest(t, server, handlers.V1LicensePath)

assert.Equal(t, http.StatusInternalServerError, recorder.Code)
assert.Equal(t, "", recorder.Body.String())
Expand All @@ -139,8 +142,8 @@ func TestRoutes(t *testing.T) {
t.Run("tasks - happy case", func(t *testing.T) {
_, mockAgentState, _, server := testSetup(t)

tasksResponse := &TasksResponse{
Tasks: []*TaskResponse{
tasksResponse := &v1.TasksResponse{
Tasks: []*v1.TaskResponse{
{
Arn: "task/arn/123",
},
Expand All @@ -151,7 +154,7 @@ func TestRoutes(t *testing.T) {
GetTasksMetadata().
Return(tasksResponse, nil)

recorder := performMockRequest(t, server, tasksMetadataPath)
recorder := performMockRequest(t, server, handlers.V1TasksMetadataPath)

testAgentMetadataJSON, _ := json.Marshal(tasksResponse)

Expand Down
7 changes: 4 additions & 3 deletions ecs-agent/introspection/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"testing"
"time"

mock_v1 "github.com/aws/amazon-ecs-agent/ecs-agent/introspection/v1/mocks"
mock_metrics "github.com/aws/amazon-ecs-agent/ecs-agent/metrics/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
Expand All @@ -33,7 +34,7 @@ var runtimeStatsConfigForTest = false

func TestNewServerErrors(t *testing.T) {
ctrl := gomock.NewController(t)
agentState := NewMockAgentState(ctrl)
agentState := mock_v1.NewMockAgentState(ctrl)
metricsFactory := mock_metrics.NewMockEntryFactory(ctrl)
t.Run("state is required", func(t *testing.T) {
_, err := NewServer(nil, metricsFactory)
Expand All @@ -47,7 +48,7 @@ func TestNewServerErrors(t *testing.T) {

func TestNewServerDefaults(t *testing.T) {
ctrl := gomock.NewController(t)
agentState := NewMockAgentState(ctrl)
agentState := mock_v1.NewMockAgentState(ctrl)
metricsFactory := mock_metrics.NewMockEntryFactory(ctrl)
t.Run("read/write defaults", func(t *testing.T) {
server, err := NewServer(agentState, metricsFactory)
Expand Down Expand Up @@ -103,7 +104,7 @@ func setupMockPprofHandlers() func() {

func TestPProfHandlerSetup(t *testing.T) {
ctrl := gomock.NewController(t)
agentState := NewMockAgentState(ctrl)
agentState := mock_v1.NewMockAgentState(ctrl)
metricsFactory := mock_metrics.NewMockEntryFactory(ctrl)

pprofPaths := []string{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

//go:generate mockgen -destination=state_mock.go -copyright_file=../../scripts/copyright_file -package=introspection . AgentState
package introspection
//go:generate mockgen -destination=./mocks/state_mock.go -copyright_file=../../../scripts/copyright_file -package=mock_v1 . AgentState
package v1
Loading

0 comments on commit d790772

Please sign in to comment.