Skip to content

Commit

Permalink
Consume common TMDS models from ecs-agent
Browse files Browse the repository at this point in the history
  • Loading branch information
amogh09 committed May 18, 2023
1 parent e3f3123 commit 51d3dbd
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 87 deletions.
2 changes: 1 addition & 1 deletion agent/handlers/task_server_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import (
"github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs"
mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks"
task_protection_v1 "github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/handlers"
v2 "github.com/aws/amazon-ecs-agent/agent/handlers/v2"
v3 "github.com/aws/amazon-ecs-agent/agent/handlers/v3"
v4 "github.com/aws/amazon-ecs-agent/agent/handlers/v4"
"github.com/aws/amazon-ecs-agent/agent/stats"
Expand All @@ -51,6 +50,7 @@ import (
tmdsresponse "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/response"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
tmdsv1 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v1"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/aws/aws-sdk-go/aws"
"github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
Expand Down
107 changes: 29 additions & 78 deletions agent/handlers/v2/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,86 +14,22 @@
package v2

import (
"time"

"github.com/aws/aws-sdk-go/aws/awserr"

"github.com/aws/amazon-ecs-agent/agent/api"
apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status"
"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
v1 "github.com/aws/amazon-ecs-agent/agent/handlers/v1"
apieni "github.com/aws/amazon-ecs-agent/ecs-agent/api/eni"
tmdsresponse "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/response"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
tmdsv2 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/aws/aws-sdk-go/aws"
"github.com/cihub/seelog"
"github.com/pkg/errors"
)

// TaskResponse defines the schema for the task response JSON object
type TaskResponse struct {
Cluster string `json:"Cluster"`
TaskARN string `json:"TaskARN"`
Family string `json:"Family"`
Revision string `json:"Revision"`
DesiredStatus string `json:"DesiredStatus,omitempty"`
KnownStatus string `json:"KnownStatus"`
Containers []ContainerResponse `json:"Containers,omitempty"`
Limits *LimitsResponse `json:"Limits,omitempty"`
PullStartedAt *time.Time `json:"PullStartedAt,omitempty"`
PullStoppedAt *time.Time `json:"PullStoppedAt,omitempty"`
ExecutionStoppedAt *time.Time `json:"ExecutionStoppedAt,omitempty"`
AvailabilityZone string `json:"AvailabilityZone,omitempty"`
TaskTags map[string]string `json:"TaskTags,omitempty"`
ContainerInstanceTags map[string]string `json:"ContainerInstanceTags,omitempty"`
LaunchType string `json:"LaunchType,omitempty"`
Errors []ErrorResponse `json:"Errors,omitempty"`
}

// ContainerResponse defines the schema for the container response
// JSON object
type ContainerResponse struct {
ID string `json:"DockerId"`
Name string `json:"Name"`
DockerName string `json:"DockerName"`
Image string `json:"Image"`
ImageID string `json:"ImageID"`
Ports []tmdsresponse.PortResponse `json:"Ports,omitempty"`
Labels map[string]string `json:"Labels,omitempty"`
DesiredStatus string `json:"DesiredStatus"`
KnownStatus string `json:"KnownStatus"`
ExitCode *int `json:"ExitCode,omitempty"`
Limits LimitsResponse `json:"Limits"`
CreatedAt *time.Time `json:"CreatedAt,omitempty"`
StartedAt *time.Time `json:"StartedAt,omitempty"`
FinishedAt *time.Time `json:"FinishedAt,omitempty"`
Type string `json:"Type"`
Networks []tmdsresponse.Network `json:"Networks,omitempty"`
Health *apicontainer.HealthStatus `json:"Health,omitempty"`
Volumes []tmdsresponse.VolumeResponse `json:"Volumes,omitempty"`
LogDriver string `json:"LogDriver,omitempty"`
LogOptions map[string]string `json:"LogOptions,omitempty"`
ContainerARN string `json:"ContainerARN,omitempty"`
}

// LimitsResponse defines the schema for task/cpu limits response
// JSON object
type LimitsResponse struct {
CPU *float64 `json:"CPU,omitempty"`
Memory *int64 `json:"Memory,omitempty"`
}

// ErrorResponse defined the schema for error response
// JSON object
type ErrorResponse struct {
ErrorField string `json:"ErrorField,omitempty"`
ErrorCode string `json:"ErrorCode,omitempty"`
ErrorMessage string `json:"ErrorMessage,omitempty"`
StatusCode int `json:"StatusCode,omitempty"`
RequestId string `json:"RequestId,omitempty"`
ResourceARN string `json:"ResourceARN,omitempty"`
}

// Agent versions >= 1.2.0: Null, zero, and CPU values of 1
// are passed to Docker as two CPU shares
const minimumCPUUnit = 2
Expand All @@ -108,13 +44,13 @@ func NewTaskResponse(
containerInstanceArn string,
propagateTags bool,
includeV4Metadata bool,
) (*TaskResponse, error) {
) (*tmdsv2.TaskResponse, error) {
task, ok := state.TaskByArn(taskARN)
if !ok {
return nil, errors.Errorf("v2 task response: unable to find task '%s'", taskARN)
}

resp := &TaskResponse{
resp := &tmdsv2.TaskResponse{
Cluster: cluster,
TaskARN: task.Arn,
Family: task.Family,
Expand All @@ -130,7 +66,7 @@ func NewTaskResponse(
taskCPU := task.CPU
taskMemory := task.Memory
if taskCPU != 0 || taskMemory != 0 {
taskLimits := &LimitsResponse{}
taskLimits := &tmdsv2.LimitsResponse{}
if taskCPU != 0 {
taskLimits.CPU = &taskCPU
}
Expand Down Expand Up @@ -169,7 +105,7 @@ func NewTaskResponse(
}

// propagateTagsToMetadata retrieves container instance and task tags from ECS
func propagateTagsToMetadata(ecsClient api.ECSClient, containerInstanceARN, taskARN string, resp *TaskResponse, includeV4Metadata bool) {
func propagateTagsToMetadata(ecsClient api.ECSClient, containerInstanceARN, taskARN string, resp *tmdsv2.TaskResponse, includeV4Metadata bool) {
containerInstanceTags, err := ecsClient.GetResourceTags(containerInstanceARN)

if err == nil {
Expand Down Expand Up @@ -198,7 +134,7 @@ func NewContainerResponseFromState(
containerID string,
state dockerstate.TaskEngineState,
includeV4Metadata bool,
) (*ContainerResponse, error) {
) (*tmdsv2.ContainerResponse, error) {
dockerContainer, ok := state.ContainerByID(containerID)
if !ok {
return nil, errors.Errorf(
Expand All @@ -220,17 +156,17 @@ func NewContainerResponse(
dockerContainer *apicontainer.DockerContainer,
eni *apieni.ENI,
includeV4Metadata bool,
) ContainerResponse {
) tmdsv2.ContainerResponse {
container := dockerContainer.Container
resp := ContainerResponse{
resp := tmdsv2.ContainerResponse{
ID: dockerContainer.DockerID,
Name: container.Name,
DockerName: dockerContainer.DockerName,
Image: container.Image,
ImageID: container.ImageID,
DesiredStatus: container.GetDesiredStatus().String(),
KnownStatus: container.GetKnownStatus().String(),
Limits: LimitsResponse{
Limits: tmdsv2.LimitsResponse{
CPU: aws.Float64(float64(container.CPU)),
Memory: aws.Int64(int64(container.Memory)),
},
Expand All @@ -255,7 +191,7 @@ func NewContainerResponse(
// Write the container health status inside the container
if dockerContainer.Container.HealthStatusShouldBeReported() {
health := dockerContainer.Container.GetHealthStatus()
resp.Health = &health
resp.Health = dockerContainerHealthToV2Health(health)
}

if createdAt := container.GetCreatedAt(); !createdAt.IsZero() {
Expand Down Expand Up @@ -302,9 +238,24 @@ func NewContainerResponse(
return resp
}

// Converts apicontainer HealthStatus type to v2 Metadata HealthStatus type
func dockerContainerHealthToV2Health(health apicontainer.HealthStatus) *tmdsv2.HealthStatus {
status := health.Status.String()
if health.Status == apicontainerstatus.ContainerHealthUnknown {
// Skip sending status if it is unknown
status = ""
}
return &tmdsv2.HealthStatus{
Status: status,
Since: health.Since,
ExitCode: health.ExitCode,
Output: health.Output,
}
}

// metadataErrorHandling writes an error to the logger, and append an error response
// to V4 metadata endpoint task response
func metadataErrorHandling(resp *TaskResponse, err error, field, resourceARN string, includeV4Metadata bool) {
func metadataErrorHandling(resp *tmdsv2.TaskResponse, err error, field, resourceARN string, includeV4Metadata bool) {
seelog.Errorf("Task Metadata error: unable to get '%s' for '%s': %s", field, resourceARN, err.Error())
if includeV4Metadata {
errResp := newErrorResponse(err, field, resourceARN)
Expand All @@ -313,8 +264,8 @@ func metadataErrorHandling(resp *TaskResponse, err error, field, resourceARN str
}

// newErrorResponse creates a new error response
func newErrorResponse(err error, field, resourceARN string) *ErrorResponse {
errResp := &ErrorResponse{
func newErrorResponse(err error, field, resourceARN string) *tmdsv2.ErrorResponse {
errResp := &tmdsv2.ErrorResponse{
ErrorField: field,
ErrorMessage: err.Error(),
ResourceARN: resourceARN,
Expand Down
63 changes: 59 additions & 4 deletions agent/handlers/v2/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ import (
"github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs"
mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks"
apieni "github.com/aws/amazon-ecs-agent/ecs-agent/api/eni"
tmdsv2 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
Expand Down Expand Up @@ -310,6 +312,10 @@ func TestContainerResponse(t *testing.T) {
},
},
}
expectedHealthStatus := tmdsv2.HealthStatus{
Status: "HEALTHY",
Since: container.Health.Since,
}
gomock.InOrder(
state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true),
state.EXPECT().TaskByID(containerID).Return(task, true),
Expand All @@ -318,6 +324,9 @@ func TestContainerResponse(t *testing.T) {
containerResponse, err := NewContainerResponseFromState(containerID, state, false)
assert.NoError(t, err)
assert.Equal(t, containerResponse.Health == nil, tc.result)
if !tc.result {
assert.Equal(t, *containerResponse.Health, expectedHealthStatus)
}
_, err = json.Marshal(containerResponse)
assert.NoError(t, err)
})
Expand Down Expand Up @@ -431,21 +440,21 @@ func TestTaskResponseMarshal(t *testing.T) {
state.EXPECT().TaskByArn(taskARN).Return(task, true),
state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true),
ecsClient.EXPECT().GetResourceTags(containerInstanceArn).Return([]*ecs.Tag{
&ecs.Tag{
{
Key: &contInstTag1Key,
Value: &contInstTag1Val,
},
&ecs.Tag{
{
Key: &contInstTag2Key,
Value: &contInstTag2Val,
},
}, nil),
ecsClient.EXPECT().GetResourceTags(taskARN).Return([]*ecs.Tag{
&ecs.Tag{
{
Key: &taskTag1Key,
Value: &taskTag1Val,
},
&ecs.Tag{
{
Key: &taskTag2Key,
Value: &taskTag2Val,
},
Expand Down Expand Up @@ -688,3 +697,49 @@ func TestTaskResponseWithV4TagsError(t *testing.T) {
assert.Equal(t, taskWithTagsResponse.Errors[1].RequestId, taskTagsRequestId)
assert.Equal(t, taskWithTagsResponse.Errors[1].ResourceARN, taskARN)
}

// Tests that TMDS Health Status created by metadata endpoint v2 is marshaled to the same JSON
// as the source container health status. This test makes sure that the change in TMDS response
// model from using container health status directly to using a new TMDS Health Status type
// is transparent to customers.
func TestDockerContainerHealthToV2HealthJSON(t *testing.T) {
tcs := []struct {
name string
status apicontainerstatus.ContainerHealthStatus
}{
{
name: "container healthy",
status: apicontainerstatus.ContainerHealthy,
},
{
name: "container unhealthy",
status: apicontainerstatus.ContainerUnhealthy,
},
{
name: "container health unknown",
status: apicontainerstatus.ContainerHealthUnknown,
},
{
name: "container health invalid",
status: 2345,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
now := time.Now()
apiHealth := &apicontainer.HealthStatus{
Status: tc.status,
Since: &now,
ExitCode: 5,
Output: "some output",
}
apiHealthJSON, err := json.Marshal(apiHealth)
require.NoError(t, err)

tmdsHealthJSON, err := json.Marshal(dockerContainerHealthToV2Health(*apiHealth))
require.NoError(t, err)

assert.Equal(t, apiHealthJSON, tmdsHealthJSON)
})
}
}
3 changes: 2 additions & 1 deletion agent/handlers/v3/container_metadata_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
v2 "github.com/aws/amazon-ecs-agent/agent/handlers/v2"
tmdsresponse "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/response"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
tmdsv2 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/cihub/seelog"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -62,7 +63,7 @@ func ContainerMetadataHandler(state dockerstate.TaskEngineState) func(http.Respo
}

// GetContainerResponse gets container response for v3 metadata
func GetContainerResponse(containerID string, state dockerstate.TaskEngineState) (*v2.ContainerResponse, error) {
func GetContainerResponse(containerID string, state dockerstate.TaskEngineState) (*tmdsv2.ContainerResponse, error) {
containerResponse, err := v2.NewContainerResponseFromState(containerID, state, false)
if err != nil {
seelog.Errorf("Unable to get container metadata for container '%s'", containerID)
Expand Down
3 changes: 2 additions & 1 deletion agent/handlers/v3/task_metadata_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
v2 "github.com/aws/amazon-ecs-agent/agent/handlers/v2"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
tmdsv2 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/cihub/seelog"
)

Expand Down Expand Up @@ -64,7 +65,7 @@ func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSCli
task, _ := state.TaskByArn(taskARN)
if !task.IsNetworkModeAWSVPC() {
// fill in non-awsvpc network details for container responses here
responses := make([]v2.ContainerResponse, 0)
responses := make([]tmdsv2.ContainerResponse, 0)
for _, containerResponse := range taskResponse.Containers {
networks, err := GetContainerNetworkMetadata(containerResponse.ID, state)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions agent/handlers/v4/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import (
apieni "github.com/aws/amazon-ecs-agent/ecs-agent/api/eni"
tmdsresponse "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/response"
"github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
tmdsv2 "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/v2"
"github.com/pkg/errors"
)

// TaskResponse is the v4 Task response. It augments the v4 Container response
// with the v2 task response object.
type TaskResponse struct {
*v2.TaskResponse
*tmdsv2.TaskResponse
Containers []ContainerResponse `json:"Containers,omitempty"`
VPCID string `json:"VPCID,omitempty"`
ServiceName string `json:"ServiceName,omitempty"`
Expand All @@ -37,7 +38,7 @@ type TaskResponse struct {
// ContainerResponse is the v4 Container response. It augments the v4 Network response
// with the v2 container response object.
type ContainerResponse struct {
*v2.ContainerResponse
*tmdsv2.ContainerResponse
Networks []Network `json:"Networks,omitempty"`
}

Expand Down
Loading

0 comments on commit 51d3dbd

Please sign in to comment.