diff --git a/agent/api/container/container.go b/agent/api/container/container.go index 21f2bf52fb3..1b5d5982804 100644 --- a/agent/api/container/container.go +++ b/agent/api/container/container.go @@ -216,6 +216,12 @@ type Container struct { // VolumesUnsafe is an array of volume mounts in the container. VolumesUnsafe []types.MountPoint `json:"-"` + // NetworkModeUnsafe is the network mode in which the container is started + NetworkModeUnsafe string `json:"-"` + + // NetworksUnsafe denotes the Docker Network Settings in the container. + NetworkSettingsUnsafe *types.NetworkSettings `json:"-"` + // SteadyStateStatusUnsafe specifies the steady state status for the container // If uninitialized, it's assumed to be set to 'ContainerRunning'. Even though // it's not only supposed to be set when the container is being created, it's @@ -593,6 +599,38 @@ func (c *Container) GetVolumes() []types.MountPoint { return c.VolumesUnsafe } +// SetNetworkSettings sets the networks field in a container +func (c *Container) SetNetworkSettings(networks *types.NetworkSettings) { + c.lock.Lock() + defer c.lock.Unlock() + + c.NetworkSettingsUnsafe = networks +} + +// GetNetworkSettings returns the networks field in a container +func (c *Container) GetNetworkSettings() *types.NetworkSettings { + c.lock.RLock() + defer c.lock.RUnlock() + + return c.NetworkSettingsUnsafe +} + +// SetNetworkMode sets the network mode of the container +func (c *Container) SetNetworkMode(networkMode string) { + c.lock.Lock() + defer c.lock.Unlock() + + c.NetworkModeUnsafe = networkMode +} + +// GetNetworkMode returns the network mode of the container +func (c *Container) GetNetworkMode() string { + c.lock.RLock() + defer c.lock.RUnlock() + + return c.NetworkModeUnsafe +} + // HealthStatusShouldBeReported returns true if the health check is defined in // the task definition func (c *Container) HealthStatusShouldBeReported() bool { diff --git a/agent/dockerclient/dockerapi/docker_client.go b/agent/dockerclient/dockerapi/docker_client.go index 2a787fe99d1..2eb0a4c17ed 100644 --- a/agent/dockerclient/dockerapi/docker_client.go +++ b/agent/dockerclient/dockerapi/docker_client.go @@ -771,6 +771,15 @@ func MetadataFromContainer(dockerContainer *types.ContainerJSON) DockerContainer StartedAt: startedTime, FinishedAt: finishedTime, } + + if dockerContainer.NetworkSettings != nil { + metadata.NetworkSettings = dockerContainer.NetworkSettings + } + + if dockerContainer.HostConfig != nil { + metadata.NetworkMode = string(dockerContainer.HostConfig.NetworkMode) + } + if dockerContainer.Config != nil { metadata.Labels = dockerContainer.Config.Labels } diff --git a/agent/dockerclient/dockerapi/docker_client_test.go b/agent/dockerclient/dockerapi/docker_client_test.go index cd235cb4477..2a487aa8b44 100644 --- a/agent/dockerclient/dockerapi/docker_client_test.go +++ b/agent/dockerclient/dockerapi/docker_client_test.go @@ -1425,6 +1425,9 @@ func TestMetadataFromContainer(t *testing.T) { NetworkSettingsBase: types.NetworkSettingsBase{ Ports: ports, }, + DefaultNetworkSettings: types.DefaultNetworkSettings{ + IPAddress: "17.0.0.3", + }, }, ContainerJSONBase: &types.ContainerJSONBase{ ID: "1234", @@ -1434,6 +1437,9 @@ func TestMetadataFromContainer(t *testing.T) { StartedAt: started, FinishedAt: finished, }, + HostConfig: &dockercontainer.HostConfig{ + NetworkMode: dockercontainer.NetworkMode("bridge"), + }, }, Config: &dockercontainer.Config{ Labels: labels, @@ -1446,6 +1452,9 @@ func TestMetadataFromContainer(t *testing.T) { assert.Equal(t, volumes, metadata.Volumes) assert.Equal(t, labels, metadata.Labels) assert.Len(t, metadata.PortBindings, 1) + assert.Equal(t, "bridge", metadata.NetworkMode) + assert.NotNil(t, metadata.NetworkSettings) + assert.Equal(t, "17.0.0.3", metadata.NetworkSettings.IPAddress) // Need to convert both strings to same format to be able to compare. Parse and Format are not inverses. createdTimeSDK, _ := time.Parse(time.RFC3339, dockerContainer.Created) diff --git a/agent/dockerclient/dockerapi/types.go b/agent/dockerclient/dockerapi/types.go index fbe950d677b..a7a5c960d61 100644 --- a/agent/dockerclient/dockerapi/types.go +++ b/agent/dockerclient/dockerapi/types.go @@ -71,6 +71,10 @@ type DockerContainerMetadata struct { FinishedAt time.Time // Health contains the result of a container health check Health apicontainer.HealthStatus + // NetworkMode denotes the network mode in which the container is started + NetworkMode string + // NetworksUnsafe denotes the Docker Network Settings in the container + NetworkSettings *types.NetworkSettings } // ListContainersResponse encapsulates the response from the docker client for the diff --git a/agent/engine/docker_task_engine.go b/agent/engine/docker_task_engine.go index f991fb988cf..18735e61f6a 100644 --- a/agent/engine/docker_task_engine.go +++ b/agent/engine/docker_task_engine.go @@ -355,6 +355,8 @@ func updateContainerMetadata(metadata *dockerapi.DockerContainerMetadata, contai if container.HealthStatusShouldBeReported() { container.SetHealthStatus(metadata.Health) } + container.SetNetworkMode(metadata.NetworkMode) + container.SetNetworkSettings(metadata.NetworkSettings) } // synchronizeContainerStatus checks and updates the container status with docker diff --git a/agent/handlers/task_server_setup_test.go b/agent/handlers/task_server_setup_test.go index 93cb7c03f02..cb024c23d14 100644 --- a/agent/handlers/task_server_setup_test.go +++ b/agent/handlers/task_server_setup_test.go @@ -83,6 +83,8 @@ const ( associationName = "dev1" associationEncoding = "base64" associationValue = "val" + bridgeMode = "bridge" + bridgeIPAddr = "17.0.0.3" ) var ( @@ -192,10 +194,99 @@ var ( Associations: []string{associationName}, } expectedAssociationResponse = associationValue + + bridgeTask = &apitask.Task{ + Arn: taskARN, + Associations: []apitask.Association{association}, + Family: family, + Version: version, + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + KnownStatusUnsafe: apitaskstatus.TaskRunning, + CPU: cpu, + Memory: memory, + PullStartedAtUnsafe: now, + PullStoppedAtUnsafe: now, + ExecutionStoppedAtUnsafe: now, + } + container1 = &apicontainer.Container{ + Name: containerName, + Image: imageName, + ImageID: imageID, + DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, + KnownStatusUnsafe: apicontainerstatus.ContainerRunning, + CPU: cpu, + Memory: memory, + Type: apicontainer.ContainerNormal, + Ports: []apicontainer.PortBinding{ + { + ContainerPort: containerPort, + Protocol: apicontainer.TransportProtocolTCP, + }, + }, + NetworkModeUnsafe: bridgeMode, + NetworkSettingsUnsafe: &types.NetworkSettings{ + DefaultNetworkSettings: types.DefaultNetworkSettings{ + IPAddress: bridgeIPAddr, + }, + }, + } + bridgeContainer = &apicontainer.DockerContainer{ + DockerID: containerID, + DockerName: containerName, + Container: container1, + } + containerNameToBridgeContainer = map[string]*apicontainer.DockerContainer{ + taskARN: bridgeContainer, + } + expectedBridgeContainerResponse = v2.ContainerResponse{ + ID: containerID, + Name: containerName, + DockerName: containerName, + Image: imageName, + ImageID: imageID, + DesiredStatus: statusRunning, + KnownStatus: statusRunning, + Limits: v2.LimitsResponse{ + CPU: aws.Float64(cpu), + Memory: aws.Int64(memory), + }, + Type: containerType, + Labels: labels, + Ports: []v1.PortResponse{ + { + ContainerPort: containerPort, + Protocol: containerPortProtocol, + }, + }, + Networks: []containermetadata.Network{ + { + NetworkMode: bridgeMode, + IPv4Addresses: []string{bridgeIPAddr}, + }, + }, + } + expectedBridgeTaskResponse = v2.TaskResponse{ + Cluster: clusterName, + TaskARN: taskARN, + Family: family, + Revision: version, + DesiredStatus: statusRunning, + KnownStatus: statusRunning, + Containers: []v2.ContainerResponse{expectedBridgeContainerResponse}, + Limits: &v2.LimitsResponse{ + CPU: aws.Float64(cpu), + Memory: aws.Int64(memory), + }, + PullStartedAt: aws.Time(now.UTC()), + PullStoppedAt: aws.Time(now.UTC()), + ExecutionStoppedAt: aws.Time(now.UTC()), + AvailabilityZone: availabilityzone, + } ) func init() { container.SetLabels(labels) + container1.SetLabels(labels) } // TestInvalidPath tests if HTTP status code 404 is returned when invalid path is queried. @@ -664,6 +755,7 @@ func TestV3TaskMetadata(t *testing.T) { state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), + state.EXPECT().TaskByArn(taskARN).Return(task, true), ) server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) @@ -679,6 +771,65 @@ func TestV3TaskMetadata(t *testing.T) { assert.Equal(t, expectedTaskResponse, taskResponse) } +func TestV3BridgeTaskMetadata(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + state := mock_dockerstate.NewMockTaskEngineState(ctrl) + auditLog := mock_audit.NewMockAuditLogger(ctrl) + statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) + + gomock.InOrder( + state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), + state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), + state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToBridgeContainer, true), + state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), + state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), + ) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + recorder := httptest.NewRecorder() + req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/task", nil) + server.Handler.ServeHTTP(recorder, req) + res, err := ioutil.ReadAll(recorder.Body) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, recorder.Code) + var taskResponse v2.TaskResponse + err = json.Unmarshal(res, &taskResponse) + assert.NoError(t, err) + assert.Equal(t, expectedBridgeTaskResponse, taskResponse) +} + +func TestV3BridgeContainerMetadata(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + state := mock_dockerstate.NewMockTaskEngineState(ctrl) + auditLog := mock_audit.NewMockAuditLogger(ctrl) + statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) + + gomock.InOrder( + state.EXPECT().DockerIDByV3EndpointID(v3EndpointID).Return(containerID, true), + state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), + state.EXPECT().TaskByID(containerID).Return(bridgeTask, true), + state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), + ) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + recorder := httptest.NewRecorder() + req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID, nil) + server.Handler.ServeHTTP(recorder, req) + res, err := ioutil.ReadAll(recorder.Body) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, recorder.Code) + var containerResponse v2.ContainerResponse + err = json.Unmarshal(res, &containerResponse) + assert.NoError(t, err) + assert.Equal(t, expectedBridgeContainerResponse, containerResponse) +} + // Test API calls for propagating Tags to Task Metadata func TestV3TaskMetadataWithTags(t *testing.T) { ctrl := gomock.NewController(t) @@ -734,6 +885,7 @@ func TestV3TaskMetadataWithTags(t *testing.T) { Value: &taskTag2Val, }, }, nil), + state.EXPECT().TaskByArn(taskARN).Return(task, true), ) server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) diff --git a/agent/handlers/v3/container_metadata_handler.go b/agent/handlers/v3/container_metadata_handler.go index 1f6f4bde96a..61b383da71a 100644 --- a/agent/handlers/v3/container_metadata_handler.go +++ b/agent/handlers/v3/container_metadata_handler.go @@ -18,10 +18,12 @@ import ( "fmt" "net/http" + "github.com/aws/amazon-ecs-agent/agent/containermetadata" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/handlers/utils" "github.com/aws/amazon-ecs-agent/agent/handlers/v2" "github.com/cihub/seelog" + "github.com/pkg/errors" ) // ContainerMetadataPath specifies the relative URI path for serving container metadata. @@ -37,10 +39,66 @@ func ContainerMetadataHandler(state dockerstate.TaskEngineState) func(http.Respo utils.WriteJSONToResponse(w, http.StatusBadRequest, responseJSON, utils.RequestTypeContainerMetadata) return } - + containerResponse, err := GetContainerResponse(containerID, state) + if err != nil { + errResponseJSON, _ := json.Marshal(err.Error()) + utils.WriteJSONToResponse(w, http.StatusBadRequest, errResponseJSON, utils.RequestTypeContainerMetadata) + return + } seelog.Infof("V3 container metadata handler: writing response for container '%s'", containerID) - // v3 handler shares the same container metadata response format with v2 handler. - v2.WriteContainerMetadataResponse(w, containerID, state) + responseJSON, _ := json.Marshal(containerResponse) + utils.WriteJSONToResponse(w, http.StatusOK, responseJSON, utils.RequestTypeContainerMetadata) + } +} + +// GetContainerResponse gets container response for v3 metadata +func GetContainerResponse(containerID string, state dockerstate.TaskEngineState) (*v2.ContainerResponse, error) { + containerResponse, err := v2.NewContainerResponse(containerID, state) + if err != nil { + return nil, errors.Errorf("Unable to generate metadata for container '%s'", containerID) + } + // fill in network details if not set + if containerResponse.Networks == nil { + if containerResponse.Networks, err = GetContainerNetworkMetadata(containerID, state); err != nil { + return nil, err + } + } + return containerResponse, nil +} + +// GetContainerNetworkMetadata returns the network metadata for the container +func GetContainerNetworkMetadata(containerID string, state dockerstate.TaskEngineState) ([]containermetadata.Network, error) { + dockerContainer, ok := state.ContainerByID(containerID) + if !ok { + return nil, errors.Errorf("Unable to find container '%s'", containerID) + } + // the logic here has been reused from + // https://github.com/aws/amazon-ecs-agent/blob/0c8913ba33965cf6ffdd6253fad422458d9346bd/agent/containermetadata/parse_metadata.go#L123 + settings := dockerContainer.Container.GetNetworkSettings() + if settings == nil { + return nil, errors.Errorf("Unable to generate network response for container '%s'", containerID) + } + // This metadata is the information provided in older versions of the API + // We get the NetworkMode (Network interface name) from the HostConfig because this + // this is the network with which the container is created + ipv4AddressFromSettings := settings.IPAddress + networkModeFromHostConfig := dockerContainer.Container.GetNetworkMode() + + // Extensive Network information is not available for Docker API versions 1.17-1.20 + // Instead we only get the details of the first network + networks := make([]containermetadata.Network, 0) + if len(settings.Networks) > 0 { + for modeFromSettings, containerNetwork := range settings.Networks { + networkMode := modeFromSettings + ipv4Addresses := []string{containerNetwork.IPAddress} + network := containermetadata.Network{NetworkMode: networkMode, IPv4Addresses: ipv4Addresses} + networks = append(networks, network) + } + } else { + ipv4Addresses := []string{ipv4AddressFromSettings} + network := containermetadata.Network{NetworkMode: networkModeFromHostConfig, IPv4Addresses: ipv4Addresses} + networks = append(networks, network) } + return networks, nil } diff --git a/agent/handlers/v3/task_metadata_handler.go b/agent/handlers/v3/task_metadata_handler.go index 8f8f6786950..7553d2a1ba4 100644 --- a/agent/handlers/v3/task_metadata_handler.go +++ b/agent/handlers/v3/task_metadata_handler.go @@ -48,7 +48,29 @@ func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSCli seelog.Infof("V3 task metadata handler: writing response for task '%s'", taskARN) - // v3 handler shares the same task metadata response format with v2 handler. - v2.WriteTaskMetadataResponse(w, taskARN, cluster, state, ecsClient, az, containerInstanceArn, propagateTags) + taskResponse, err := v2.NewTaskResponse(taskARN, state, ecsClient, cluster, az, containerInstanceArn, propagateTags) + if err != nil { + errResponseJSON, _ := json.Marshal("Unable to generate metadata for task: '" + taskARN + "'") + utils.WriteJSONToResponse(w, http.StatusBadRequest, errResponseJSON, utils.RequestTypeTaskMetadata) + return + } + task, _ := state.TaskByArn(taskARN) + if task.GetTaskENI() == nil { + // fill in non-awsvpc network details for container responses here + responses := make([]v2.ContainerResponse, 0) + for _, containerResponse := range taskResponse.Containers { + networks, err := GetContainerNetworkMetadata(containerResponse.ID, state) + if err != nil { + errResponseJSON, _ := json.Marshal(err.Error()) + utils.WriteJSONToResponse(w, http.StatusBadRequest, errResponseJSON, utils.RequestTypeContainerMetadata) + return + } + containerResponse.Networks = networks + responses = append(responses, containerResponse) + } + taskResponse.Containers = responses + } + responseJSON, _ := json.Marshal(taskResponse) + utils.WriteJSONToResponse(w, http.StatusOK, responseJSON, utils.RequestTypeTaskMetadata) } } diff --git a/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go b/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go index e620690c2de..aed84f50c29 100644 --- a/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go +++ b/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go @@ -22,6 +22,7 @@ import ( "time" "github.com/docker/docker/api/types" + "github.com/pkg/errors" ) const ( @@ -32,6 +33,7 @@ const ( var isAWSVPCNetworkMode bool var checkContainerInstanceTags bool +var networkModes map[string]bool // TaskResponse defines the schema for the task response JSON object type TaskResponse struct { @@ -264,7 +266,7 @@ func verifyContainerMetadataResponse(containerMetadataRawMsg json.RawMessage) er "Type": "NORMAL", } - taskExpectedFieldNotEmptyArray := []string{"DockerId", "DockerName", "ImageID", "Limits", "CreatedAt", "StartedAt", "Health"} + taskExpectedFieldNotEmptyArray := []string{"DockerId", "DockerName", "ImageID", "Limits", "CreatedAt", "StartedAt", "Health", "Networks"} for fieldName, fieldVal := range containerExpectedFieldEqualMap { if err = fieldEqual(containerMetadataResponseMap, fieldName, fieldVal); err != nil { @@ -308,10 +310,6 @@ func verifyLimitResponse(limitRawMsg json.RawMessage) error { } func verifyNetworksResponse(networksRawMsg json.RawMessage) error { - // host and bridge network mode - if networksRawMsg == nil { - return nil - } var err error @@ -322,23 +320,28 @@ func verifyNetworksResponse(networksRawMsg json.RawMessage) error { networkResponseMap := make(map[string]json.RawMessage) json.Unmarshal(networksResponseArray[0], &networkResponseMap) - if err = fieldEqual(networkResponseMap, "NetworkMode", "awsvpc"); err != nil { - return err - } + var actualFieldVal interface{} + json.Unmarshal(networkResponseMap["NetworkMode"], &actualFieldVal) - if err = fieldNotEmpty(networkResponseMap, "IPv4Addresses"); err != nil { - return err + if _, ok := networkModes[actualFieldVal.(string)]; !ok { + return errors.Errorf("network mode is incorrect: %s", actualFieldVal) } - - var ipv4AddressesResponseArray []json.RawMessage - json.Unmarshal(networkResponseMap["IPv4Addresses"], &ipv4AddressesResponseArray) - - if len(ipv4AddressesResponseArray) != 1 { - return fmt.Errorf("incorrect number of IPv4Addresses, expected 1, received %d", - len(ipv4AddressesResponseArray)) + if actualFieldVal != "host" { + if err = fieldNotEmpty(networkResponseMap, "IPv4Addresses"); err != nil { + return err + } + + var ipv4AddressesResponseArray []json.RawMessage + json.Unmarshal(networkResponseMap["IPv4Addresses"], &ipv4AddressesResponseArray) + + if len(ipv4AddressesResponseArray) != 1 { + return fmt.Errorf("incorrect number of IPv4Addresses, expected 1, received %d", + len(ipv4AddressesResponseArray)) + } + } + if actualFieldVal == "awsvpc" { + isAWSVPCNetworkMode = true } - - isAWSVPCNetworkMode = true } else { return fmt.Errorf("incorrect number of networks, expected 1, received %d", len(networksResponseArray)) @@ -415,6 +418,8 @@ func main() { Timeout: 5 * time.Second, } + networkModes = map[string]bool{"awsvpc": true, "bridge": true, "host": true, "default": true} + // If the image is built with option to check Tags argsWithoutProg := os.Args[1:] if len(argsWithoutProg) > 0 {