From 070b8ca018a04c27e6adcbd5399bd685329c7847 Mon Sep 17 00:00:00 2001 From: Kar Shin Lin Date: Mon, 3 Dec 2018 18:12:33 -0800 Subject: [PATCH] Progagating Container Instance and Task Tags to Task Metadata endpoint; Bumping functional test timeout from 30m to 32m --- Makefile | 2 +- agent/api/ecsclient/client.go | 10 + agent/api/interface.go | 3 + agent/api/mocks/api_mocks.go | 26 ++ agent/app/agent.go | 2 +- agent/ecs_client/model/api/api-2.json | 19 +- agent/ecs_client/model/api/docs-2.json | 27 +- agent/ecs_client/model/ecs/api.go | 39 ++- .../dockerstate/docker_task_engine_state.go | 1 + .../task-definition.json | 1 + .../task-definition.json | 2 +- .../task-definition.json | 1 + .../tests/functionaltests_test.go | 76 +++++ .../tests/functionaltests_unix_test.go | 27 +- .../tests/functionaltests_windows_test.go | 4 + agent/handlers/task_server_setup.go | 33 ++- agent/handlers/task_server_setup_test.go | 271 ++++++++++++++---- agent/handlers/v2/response.go | 58 +++- agent/handlers/v2/response_test.go | 76 +++-- .../v2/task_container_metadata_handler.go | 15 +- agent/handlers/v3/task_metadata_handler.go | 9 +- .../taskmetadata-validator.go | 61 +++- .../v3-task-endpoint-validator-windows.go | 20 +- misc/v3-task-endpoint-validator/Dockerfile | 1 - .../v3-task-endpoint-validator.go | 19 +- 25 files changed, 680 insertions(+), 123 deletions(-) diff --git a/Makefile b/Makefile index 7f374645d98..e7f7df8919e 100644 --- a/Makefile +++ b/Makefile @@ -193,7 +193,7 @@ test-in-docker: docker run --net=none -v "$(PWD):/go/src/github.com/aws/amazon-ecs-agent" --privileged "amazon/amazon-ecs-agent-test:make" run-functional-tests: testnnp test-registry ecr-execution-role-image telemetry-test-image - . ./scripts/shared_env && go test -tags functional -timeout=30m -v ./agent/functional_tests/... + . ./scripts/shared_env && go test -tags functional -timeout=32m -v ./agent/functional_tests/... .PHONY: build-image-for-ecr ecr-execution-role-image-for-upload upload-images replicate-images diff --git a/agent/api/ecsclient/client.go b/agent/api/ecsclient/client.go index e7d247c07cb..90bfd099fe8 100644 --- a/agent/api/ecsclient/client.go +++ b/agent/api/ecsclient/client.go @@ -523,3 +523,13 @@ func (client *APIECSClient) discoverPollEndpoint(containerInstanceArn string) (* client.pollEndpoinCache.Set(containerInstanceArn, output) return output, nil } + +func (client *APIECSClient) GetResourceTags(resourceArn string) ([]*ecs.Tag, error) { + output, err := client.standardClient.ListTagsForResource(&ecs.ListTagsForResourceInput{ + ResourceArn: &resourceArn, + }) + if err != nil { + return nil, err + } + return output.Tags, nil +} diff --git a/agent/api/interface.go b/agent/api/interface.go index ef25f7f6e5a..4c3afc4f870 100644 --- a/agent/api/interface.go +++ b/agent/api/interface.go @@ -40,6 +40,8 @@ type ECSClient interface { // DiscoverTelemetryEndpoint takes a ContainerInstanceARN and returns the // endpoint at which this Agent should contact Telemetry Service DiscoverTelemetryEndpoint(containerInstanceArn string) (string, error) + // GetTaskTags retrieves the Tags associated with a certain Task + GetResourceTags(resourceArn string) ([]*ecs.Tag, error) } // ECSSDK is an interface that specifies the subset of the AWS Go SDK's ECS @@ -49,6 +51,7 @@ type ECSSDK interface { CreateCluster(*ecs.CreateClusterInput) (*ecs.CreateClusterOutput, error) RegisterContainerInstance(*ecs.RegisterContainerInstanceInput) (*ecs.RegisterContainerInstanceOutput, error) DiscoverPollEndpoint(*ecs.DiscoverPollEndpointInput) (*ecs.DiscoverPollEndpointOutput, error) + ListTagsForResource(*ecs.ListTagsForResourceInput) (*ecs.ListTagsForResourceOutput, error) } // ECSSubmitStateSDK is an interface with customized ecs client that diff --git a/agent/api/mocks/api_mocks.go b/agent/api/mocks/api_mocks.go index e1603c05ffb..22beb723729 100644 --- a/agent/api/mocks/api_mocks.go +++ b/agent/api/mocks/api_mocks.go @@ -74,6 +74,19 @@ func (mr *MockECSSDKMockRecorder) DiscoverPollEndpoint(arg0 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverPollEndpoint", reflect.TypeOf((*MockECSSDK)(nil).DiscoverPollEndpoint), arg0) } +// ListTagsForResource mocks base method +func (m *MockECSSDK) ListTagsForResource(arg0 *ecs.ListTagsForResourceInput) (*ecs.ListTagsForResourceOutput, error) { + ret := m.ctrl.Call(m, "ListTagsForResource", arg0) + ret0, _ := ret[0].(*ecs.ListTagsForResourceOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTagsForResource indicates an expected call of ListTagsForResource +func (mr *MockECSSDKMockRecorder) ListTagsForResource(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsForResource", reflect.TypeOf((*MockECSSDK)(nil).ListTagsForResource), arg0) +} + // RegisterContainerInstance mocks base method func (m *MockECSSDK) RegisterContainerInstance(arg0 *ecs.RegisterContainerInstanceInput) (*ecs.RegisterContainerInstanceOutput, error) { ret := m.ctrl.Call(m, "RegisterContainerInstance", arg0) @@ -185,6 +198,19 @@ func (mr *MockECSClientMockRecorder) DiscoverTelemetryEndpoint(arg0 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverTelemetryEndpoint", reflect.TypeOf((*MockECSClient)(nil).DiscoverTelemetryEndpoint), arg0) } +// GetResourceTags mocks base method +func (m *MockECSClient) GetResourceTags(arg0 string) ([]*ecs.Tag, error) { + ret := m.ctrl.Call(m, "GetResourceTags", arg0) + ret0, _ := ret[0].([]*ecs.Tag) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourceTags indicates an expected call of GetResourceTags +func (mr *MockECSClientMockRecorder) GetResourceTags(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceTags", reflect.TypeOf((*MockECSClient)(nil).GetResourceTags), arg0) +} + // RegisterContainerInstance mocks base method func (m *MockECSClient) RegisterContainerInstance(arg0 string, arg1 []*ecs.Attribute, arg2 []*ecs.Tag, arg3 string) (string, string, error) { ret := m.ctrl.Call(m, "RegisterContainerInstance", arg0, arg1, arg2, arg3) diff --git a/agent/app/agent.go b/agent/app/agent.go index 0dadaa4d4a6..686c7c521ff 100644 --- a/agent/app/agent.go +++ b/agent/app/agent.go @@ -559,7 +559,7 @@ func (agent *ecsAgent) startAsyncRoutines( statsEngine := stats.NewDockerStatsEngine(agent.cfg, agent.dockerClient, containerChangeEventStream) // Start serving the endpoint to fetch IAM Role credentials and other task metadata - go handlers.ServeTaskHTTPEndpoint(credentialsManager, state, agent.containerInstanceARN, agent.cfg, statsEngine, agent.availabilityZone) + go handlers.ServeTaskHTTPEndpoint(credentialsManager, state, client, agent.containerInstanceARN, agent.cfg, statsEngine, agent.availabilityZone) // Start sending events to the backend go eventhandler.HandleEngineEvents(taskEngine, client, taskHandler) diff --git a/agent/ecs_client/model/api/api-2.json b/agent/ecs_client/model/api/api-2.json index f758c0ace06..153a4a7882c 100644 --- a/agent/ecs_client/model/api/api-2.json +++ b/agent/ecs_client/model/api/api-2.json @@ -777,7 +777,8 @@ "attachments":{"shape":"Attachments"}, "clientToken":{ "shape":"String" - } + }, + "tags":{"shape":"Tags"} } }, "ContainerInstanceStatus":{ @@ -1623,6 +1624,12 @@ "stringSetValue":{"shape":"StringList"} } }, + "ResourceNotFoundException":{ + "type":"structure", + "members":{ + }, + "exception":true + }, "Resources":{ "type":"list", "member":{"shape":"Resource"} @@ -1665,6 +1672,13 @@ "DAEMON" ] }, + "Scope":{ + "type":"string", + "enum":[ + "task", + "shared" + ] + }, "Secret":{ "type":"structure", "required":[ @@ -1940,7 +1954,8 @@ "launchType":{"shape":"LaunchType"}, "platformVersion":{"shape":"String"}, "attachments":{"shape":"Attachments"}, - "healthStatus":{"shape":"HealthStatus"} + "healthStatus":{"shape":"HealthStatus"}, + "tags":{"shape":"Tags"} } }, "TaskDefinition":{ diff --git a/agent/ecs_client/model/api/docs-2.json b/agent/ecs_client/model/api/docs-2.json index 782bf92ee85..1acd1310ed0 100644 --- a/agent/ecs_client/model/api/docs-2.json +++ b/agent/ecs_client/model/api/docs-2.json @@ -676,6 +676,16 @@ "refs": { } }, + "ListTagsForResourceRequest": { + "base": null, + "refs": { + } + }, + "ListTagsForResourceResponse": { + "base": null, + "refs": { + } + }, "ListTaskDefinitionFamiliesRequest": { "base": null, "refs": { @@ -951,7 +961,13 @@ "refs": { "CreateServiceRequest$schedulingStrategy": "

The scheduling strategy to use for the service. For more information, see Services.

There are two service scheduler strategies available:

", "ListServicesRequest$schedulingStrategy": "

The scheduling strategy for services to list.

", - "Service$schedulingStrategy": "

The scheduling strategy to use for the service. For more information, see Services.

There are two service scheduler strategies available:

" + "Service$schedulingStrategy": "

The scheduling strategy to use for the service. For more information, see Services.

There are two service scheduler strategies available:

" + } + }, + "Scope": { + "base": null, + "refs": { + "DockerVolumeConfiguration$scope": "

The scope for the Docker volume that determines its lifecycle. Docker volumes that are scoped to a task are automatically provisioned when the task starts and destroyed when the task stops. Docker volumes that are scoped as shared persist after the task stops.

" } }, "Secret": { @@ -1130,6 +1146,7 @@ "ListServicesRequest$cluster": "

The short name or full Amazon Resource Name (ARN) of the cluster that hosts the services to list. If you do not specify a cluster, the default cluster is assumed.

", "ListServicesRequest$nextToken": "

The nextToken value returned from a previous paginated ListServices request where maxResults was used and the results exceeded the value of that parameter. Pagination continues from the end of the previous results that returned the nextToken value.

This token should be treated as an opaque identifier that is only used to retrieve the next items in a list and not for other programmatic purposes.

", "ListServicesResponse$nextToken": "

The nextToken value to include in a future ListServices request. When the results of a ListServices request exceed maxResults, this value can be used to retrieve the next page of results. This value is null when there are no more results to return.

", + "ListTagsForResourceRequest$resourceArn": "

The Amazon Resource Name (ARN) that identifies the resource for which to list the tags. Currently, the supported resources are Amazon ECS tasks, services, task definitions, clusters, and container instances.

", "ListTaskDefinitionFamiliesRequest$familyPrefix": "

The familyPrefix is a string that is used to filter the results of ListTaskDefinitionFamilies. If you specify a familyPrefix, only task definition family names that begin with the familyPrefix string are returned.

", "ListTaskDefinitionFamiliesRequest$nextToken": "

The nextToken value returned from a previous paginated ListTaskDefinitionFamilies request where maxResults was used and the results exceeded the value of that parameter. Pagination continues from the end of the previous results that returned the nextToken value.

This token should be treated as an opaque identifier that is only used to retrieve the next items in a list and not for other programmatic purposes.

", "ListTaskDefinitionFamiliesResponse$nextToken": "

The nextToken value to include in a future ListTaskDefinitionFamilies request. When the results of a ListTaskDefinitionFamilies request exceed maxResults, this value can be used to retrieve the next page of results. This value is null when there are no more results to return.

", @@ -1294,6 +1311,14 @@ "refs": { } }, + "Tags": { + "base": null, + "refs": { + "ContainerInstance$tags": "

The metadata that you apply to the container instance to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters.

", + "ListTagsForResourceResponse$tags": "

The tags for the resource.

", + "Task$tags": "

The metadata that you apply to the task to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters.

" + } + }, "TargetNotFoundException": { "base": "

The specified target could not be found. You can view your available container instances with ListContainerInstances. Amazon ECS container instances are cluster-specific and region-specific.

", "refs": { diff --git a/agent/ecs_client/model/ecs/api.go b/agent/ecs_client/model/ecs/api.go index 27803f25769..0e1abc293b6 100644 --- a/agent/ecs_client/model/ecs/api.go +++ b/agent/ecs_client/model/ecs/api.go @@ -4939,6 +4939,12 @@ type ContainerInstance struct { // in the Amazon Elastic Container Service Developer Guide. Status *string `locationName:"status" type:"string"` + // The metadata that you apply to the container instance to help you categorize + // and organize them. Each tag consists of a key and an optional value, both + // of which you define. Tag keys can have a maximum character length of 128 + // characters, and tag values can have a maximum length of 256 characters. + Tags []*Tag `locationName:"tags" type:"list"` + // The version counter for the container instance. Every time a container instance // experiences a change that triggers a CloudWatch event, the version counter // is incremented. If you are replicating your Amazon ECS container instance @@ -5041,6 +5047,12 @@ func (s *ContainerInstance) SetStatus(v string) *ContainerInstance { return s } +// SetTags sets the Tags field's value. +func (s *ContainerInstance) SetTags(v []*Tag) *ContainerInstance { + s.Tags = v + return s +} + // SetVersion sets the Version field's value. func (s *ContainerInstance) SetVersion(v int64) *ContainerInstance { s.Version = &v @@ -6678,6 +6690,10 @@ type DockerVolumeConfiguration struct { Labels map[string]*string `locationName:"labels" type:"map"` + // The scope for the Docker volume that determines its lifecycle. Docker volumes + // that are scoped to a task are automatically provisioned when the task starts + // and destroyed when the task stops. Docker volumes that are scoped as shared + // persist after the task stops. Scope *string `locationName:"scope" type:"string" enum:"Scope"` } @@ -7597,6 +7613,10 @@ func (s *ListServicesOutput) SetServiceArns(v []*string) *ListServicesOutput { type ListTagsForResourceInput struct { _ struct{} `type:"structure"` + // The Amazon Resource Name (ARN) that identifies the resource for which to + // list the tags. Currently, the supported resources are Amazon ECS tasks, services, + // task definitions, clusters, and container instances. + // // ResourceArn is a required field ResourceArn *string `locationName:"resourceArn" type:"string" required:"true"` } @@ -7633,6 +7653,7 @@ func (s *ListTagsForResourceInput) SetResourceArn(v string) *ListTagsForResource type ListTagsForResourceOutput struct { _ struct{} `type:"structure"` + // The tags for the resource. Tags []*Tag `locationName:"tags" type:"list"` } @@ -9564,7 +9585,7 @@ type Service struct { RunningCount *int64 `locationName:"runningCount" type:"integer"` // The scheduling strategy to use for the service. For more information, see - // Services (http://docs.aws.amazon.com/AmazonECS/latest/developerguideecs_services.html). + // Services (http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html). // // There are two service scheduler strategies available: // @@ -9574,8 +9595,8 @@ type Service struct { // and constraints to customize task placement decisions. // // * DAEMON-The daemon scheduling strategy deploys exactly one task on each - // container instance in your cluster. When using this strategy, do not specify - // a desired number of tasks or any task placement strategies. + // container instance in your cluster. When you are using this strategy, + // do not specify a desired number of tasks or any task placement strategies. // // Fargate tasks do not support the DAEMON scheduling strategy. SchedulingStrategy *string `locationName:"schedulingStrategy" type:"string" enum:"SchedulingStrategy"` @@ -10591,6 +10612,12 @@ type Task struct { // state to STOPPED). StoppingAt *time.Time `locationName:"stoppingAt" type:"timestamp"` + // The metadata that you apply to the task to help you categorize and organize + // them. Each tag consists of a key and an optional value, both of which you + // define. Tag keys can have a maximum character length of 128 characters, and + // tag values can have a maximum length of 256 characters. + Tags []*Tag `locationName:"tags" type:"list"` + // The Amazon Resource Name (ARN) of the task. TaskArn *string `locationName:"taskArn" type:"string"` @@ -10760,6 +10787,12 @@ func (s *Task) SetStoppingAt(v time.Time) *Task { return s } +// SetTags sets the Tags field's value. +func (s *Task) SetTags(v []*Tag) *Task { + s.Tags = v + return s +} + // SetTaskArn sets the TaskArn field's value. func (s *Task) SetTaskArn(v string) *Task { s.TaskArn = &v diff --git a/agent/engine/dockerstate/docker_task_engine_state.go b/agent/engine/dockerstate/docker_task_engine_state.go index a1dd7df0c4f..103513f2bcc 100644 --- a/agent/engine/dockerstate/docker_task_engine_state.go +++ b/agent/engine/dockerstate/docker_task_engine_state.go @@ -73,6 +73,7 @@ type TaskEngineState interface { DockerIDByV3EndpointID(v3EndpointID string) (string, bool) // TaskARNByV3EndpointID returns a taskARN for a given v3 endpoint ID TaskARNByV3EndpointID(v3EndpointID string) (string, bool) + json.Marshaler json.Unmarshaler } diff --git a/agent/functional_tests/testdata/taskdefinitions/taskmetadata-validator-awsvpc/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/taskmetadata-validator-awsvpc/task-definition.json index 59f1993aceb..b980c499ff9 100644 --- a/agent/functional_tests/testdata/taskdefinitions/taskmetadata-validator-awsvpc/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/taskmetadata-validator-awsvpc/task-definition.json @@ -12,6 +12,7 @@ "retries": 2, "startPeriod": 1 }, + "command": ["$$$CHECK_TAGS$$$"], "logConfiguration": { "logDriver": "awslogs", "options": { diff --git a/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator-windows/task-definition.json index b5e7ba08e6d..ad251e5bfdf 100644 --- a/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator-windows/task-definition.json @@ -13,7 +13,7 @@ "startPeriod": 1 }, "entryPoint": ["powershell"], - "command": [".\\application.ps1; $env:AWS_REGION=\"$$$TEST_REGION$$$\";.\\v3-task-endpoint-validator-windows.exe; exit $LASTEXITCODE"], + "command": [".\\application.ps1; $env:AWS_REGION=\"$$$TEST_REGION$$$\";.\\v3-task-endpoint-validator-windows.exe $$$CHECK_TAGS$$$; exit $LASTEXITCODE"], "logConfiguration": { "logDriver": "awslogs", "options": { diff --git a/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator/task-definition.json index 829a83c301b..884d8445bfc 100644 --- a/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/v3-task-endpoint-validator/task-definition.json @@ -12,6 +12,7 @@ "retries": 2, "startPeriod": 1 }, + "command": ["$$$CHECK_TAGS$$$"], "logConfiguration": { "logDriver": "awslogs", "options": { diff --git a/agent/functional_tests/tests/functionaltests_test.go b/agent/functional_tests/tests/functionaltests_test.go index d79de234825..f88309c08c9 100644 --- a/agent/functional_tests/tests/functionaltests_test.go +++ b/agent/functional_tests/tests/functionaltests_test.go @@ -590,6 +590,7 @@ func testV3TaskEndpoint(t *testing.T, taskName, containerName, networkMode, awsl tdOverrides := make(map[string]string) tdOverrides["$$$TEST_REGION$$$"] = *ECS.Config.Region tdOverrides["$$$TEST_AWSLOGS_STREAM_PREFIX$$$"] = awslogsPrefix + tdOverrides["$$$CHECK_TAGS$$$"] = "" // Tags are not checked in regular V3TaskEndpoint Test if networkMode != "" { tdOverrides["$$$NETWORK_MODE$$$"] = networkMode @@ -718,3 +719,78 @@ func TestContainerInstanceTags(t *testing.T) { _, err = ECS.DeleteAccountSetting(&DeleteAccountSettingInput) assert.NoError(t, err) } + +func testV3TaskEndpointTags(t *testing.T, taskName, containerName, networkMode string) { + // We need long container instance ARN for tagging APIs, PutAccountSettingInput + // will enable long container instance ARN. + putAccountSettingInput := ecsapi.PutAccountSettingInput{ + Name: aws.String("containerInstanceLongArnFormat"), + Value: aws.String("enabled"), + } + _, err := ECS.PutAccountSetting(&putAccountSettingInput) + assert.NoError(t, err) + + awslogsPrefix := "ecs-functional-tests-v3-task-endpoint-with-tags-validator" + agentOptions := &AgentOptions{ + ExtraEnvironment: map[string]string{ + "ECS_AVAILABLE_LOGGING_DRIVERS": `["awslogs"]`, + "ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM": "ec2_instance", + "ECS_CONTAINER_INSTANCE_TAGS": fmt.Sprintf(`{"%s": "%s"}`, + "localKey", "localValue"), + }, + PortBindings: map[docker.Port]map[string]string{ + "51679/tcp": { + "HostIP": "0.0.0.0", + "HostPort": "51679", + }, + }, + } + + agent := RunAgent(t, agentOptions) + defer agent.Cleanup() + + tdOverrides := make(map[string]string) + tdOverrides["$$$CHECK_TAGS$$$"] = "CheckTags" // To enable Tag check in v3-task-endpoint-validator image + + tdOverrides["$$$TEST_REGION$$$"] = *ECS.Config.Region + tdOverrides["$$$TEST_AWSLOGS_STREAM_PREFIX$$$"] = awslogsPrefix + tdOverrides["$$$NETWORK_MODE$$$"] = networkMode + tdOverrides["$$$TEST_AWSLOGS_STREAM_PREFIX$$$"] = tdOverrides["$$$TEST_AWSLOGS_STREAM_PREFIX$$$"] + "-" + networkMode + + task, err := agent.StartTaskWithTaskDefinitionOverrides(t, taskName, tdOverrides) + + require.NoError(t, err, "Error start task") + err = task.WaitRunning(waitTaskStateChangeDuration) + require.NoError(t, err, "Error waiting for task to run") + containerId, err := agent.ResolveTaskDockerID(task, containerName) + require.NoError(t, err, "Error resolving docker id for container in task") + + // Container should have the ExtraEnvironment variable ECS_CONTAINER_METADATA_URI + containerMetaData, err := agent.DockerClient.InspectContainer(containerId) + require.NoError(t, err, "Could not inspect container for task") + v3TaskEndpointEnabled := false + if containerMetaData.Config != nil { + for _, env := range containerMetaData.Config.Env { + if strings.HasPrefix(env, "ECS_CONTAINER_METADATA_URI=") { + v3TaskEndpointEnabled = true + break + } + } + } + if !v3TaskEndpointEnabled { + task.Stop() + t.Fatal("Could not found ECS_CONTAINER_METADATA_URI in the container environment variable") + } + + err = task.WaitStopped(waitTaskStateChangeDuration) + require.NoError(t, err, "Error waiting for task to transition to STOPPED") + + exitCode, _ := task.ContainerExitcode(containerName) + assert.Equal(t, 42, exitCode, fmt.Sprintf("Expected exit code of 42; got %d", exitCode)) + + DeleteAccountSettingInput := ecsapi.DeleteAccountSettingInput{ + Name: aws.String("containerInstanceLongArnFormat"), + } + _, err = ECS.DeleteAccountSetting(&DeleteAccountSettingInput) + assert.NoError(t, err) +} diff --git a/agent/functional_tests/tests/functionaltests_unix_test.go b/agent/functional_tests/tests/functionaltests_unix_test.go index 2140cdbff50..2d97c59d2a5 100644 --- a/agent/functional_tests/tests/functionaltests_unix_test.go +++ b/agent/functional_tests/tests/functionaltests_unix_test.go @@ -475,6 +475,10 @@ func TestV3TaskEndpointHostNetworkMode(t *testing.T) { testV3TaskEndpoint(t, "v3-task-endpoint-validator", "v3-task-endpoint-validator", "host", "ecs-functional-tests-v3-task-endpoint-validator") } +func TestV3TaskEndpointTags(t *testing.T) { + testV3TaskEndpointTags(t, "v3-task-endpoint-validator", "v3-task-endpoint-validator", "host") +} + // TestMemoryOvercommit tests the MemoryReservation of container can be configured in task definition func TestMemoryOvercommit(t *testing.T) { agent := RunAgent(t, nil) @@ -671,6 +675,17 @@ func TestAgentIntrospectionValidator(t *testing.T) { func TestTaskMetadataValidator(t *testing.T) { RequireDockerVersion(t, ">=17.06.0-ce") + + // Added to test presence of tags in metadata endpoint + // We need long container instance ARN for tagging APIs, PutAccountSettingInput + // will enable long container instance ARN. + putAccountSettingInput := ecsapi.PutAccountSettingInput{ + Name: aws.String("containerInstanceLongArnFormat"), + Value: aws.String("enabled"), + } + _, err := ECS.PutAccountSetting(&putAccountSettingInput) + assert.NoError(t, err) + // Best effort to create a log group. It should be safe to even not do this // as the log group gets created in the TestAWSLogsDriver functional test. cwlClient := cloudwatchlogs.New(session.New(), aws.NewConfig().WithRegion(*ECS.Config.Region)) @@ -680,7 +695,10 @@ func TestTaskMetadataValidator(t *testing.T) { agent := RunAgent(t, &AgentOptions{ EnableTaskENI: true, ExtraEnvironment: map[string]string{ - "ECS_AVAILABLE_LOGGING_DRIVERS": `["awslogs"]`, + "ECS_AVAILABLE_LOGGING_DRIVERS": `["awslogs"]`, + "ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM": "ec2_instance", + "ECS_CONTAINER_INSTANCE_TAGS": fmt.Sprintf(`{"%s": "%s"}`, + "localKey", "localValue"), }, }) defer agent.Cleanup() @@ -688,6 +706,7 @@ func TestTaskMetadataValidator(t *testing.T) { tdOverrides := make(map[string]string) tdOverrides["$$$TEST_REGION$$$"] = *ECS.Config.Region + tdOverrides["$$$CHECK_TAGS$$$"] = "CheckTags" // Added to test presence of tags in metadata endpoint task, err := agent.StartAWSVPCTask("taskmetadata-validator-awsvpc", tdOverrides) require.NoError(t, err, "Unable to start task with 'awsvpc' network mode") @@ -703,6 +722,12 @@ func TestTaskMetadataValidator(t *testing.T) { exitCode, _ := task.ContainerExitcode("taskmetadata-validator") assert.Equal(t, 42, exitCode, fmt.Sprintf("Expected exit code of 42; got %d", exitCode)) + + DeleteAccountSettingInput := ecsapi.DeleteAccountSettingInput{ + Name: aws.String("containerInstanceLongArnFormat"), + } + _, err = ECS.DeleteAccountSetting(&DeleteAccountSettingInput) + assert.NoError(t, err) } // TestExecutionRole verifies that task can use the execution credentials to pull from ECR and diff --git a/agent/functional_tests/tests/functionaltests_windows_test.go b/agent/functional_tests/tests/functionaltests_windows_test.go index def4a66f47c..b44039b7602 100644 --- a/agent/functional_tests/tests/functionaltests_windows_test.go +++ b/agent/functional_tests/tests/functionaltests_windows_test.go @@ -184,6 +184,10 @@ func TestV3TaskEndpointDefaultNetworkMode(t *testing.T) { testV3TaskEndpoint(t, "v3-task-endpoint-validator-windows", "v3-task-endpoint-validator-windows", "", "ecs-functional-tests-v3-task-endpoint-validator-windows") } +func TestV3TaskEndpointTags(t *testing.T) { + testV3TaskEndpointTags(t, "v3-task-endpoint-validator-windows", "v3-task-endpoint-validator-windows", "") +} + // TestMetadataServiceValidator Tests that the metadata file can be accessed from the // container using the ECS_CONTAINER_METADATA_FILE environment variables func TestMetadataServiceValidator(t *testing.T) { diff --git a/agent/handlers/task_server_setup.go b/agent/handlers/task_server_setup.go index b1b64aa894c..dc187e37e83 100644 --- a/agent/handlers/task_server_setup.go +++ b/agent/handlers/task_server_setup.go @@ -18,6 +18,7 @@ import ( "strconv" "time" + "github.com/aws/amazon-ecs-agent/agent/api" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" @@ -46,11 +47,13 @@ const ( func taskServerSetup(credentialsManager credentials.Manager, auditLogger audit.AuditLogger, state dockerstate.TaskEngineState, + ecsClient api.ECSClient, cluster string, statsEngine stats.Engine, steadyStateRate int, burstRate int, - availabilityZone string) *http.Server { + availabilityZone string, + containerInstanceArn string) *http.Server { muxRouter := mux.NewRouter() // Set this so that for request like "/v3//metadata/task", the Agent will pass @@ -60,9 +63,9 @@ func taskServerSetup(credentialsManager credentials.Manager, muxRouter.HandleFunc(v1.CredentialsPath, v1.CredentialsHandler(credentialsManager, auditLogger)) - v2HandlersSetup(muxRouter, state, statsEngine, cluster, credentialsManager, auditLogger, availabilityZone) + v2HandlersSetup(muxRouter, state, ecsClient, statsEngine, cluster, credentialsManager, auditLogger, availabilityZone, containerInstanceArn) - v3HandlersSetup(muxRouter, state, statsEngine, cluster, availabilityZone) + v3HandlersSetup(muxRouter, state, ecsClient, statsEngine, cluster, availabilityZone, containerInstanceArn) limiter := tollbooth.NewLimiter(int64(steadyStateRate), nil) limiter.SetOnLimitReached(handlersutils.LimitReachedHandler(auditLogger)) @@ -91,15 +94,19 @@ func taskServerSetup(credentialsManager credentials.Manager, // v2HandlersSetup adds all handlers in v2 package to the mux router. func v2HandlersSetup(muxRouter *mux.Router, state dockerstate.TaskEngineState, + ecsClient api.ECSClient, statsEngine stats.Engine, cluster string, credentialsManager credentials.Manager, auditLogger audit.AuditLogger, - availabilityZone string) { + availabilityZone string, + containerInstanceArn string) { muxRouter.HandleFunc(v2.CredentialsPath, v2.CredentialsHandler(credentialsManager, auditLogger)) - muxRouter.HandleFunc(v2.ContainerMetadataPath, v2.TaskContainerMetadataHandler(state, cluster, availabilityZone)) - muxRouter.HandleFunc(v2.TaskMetadataPath, v2.TaskContainerMetadataHandler(state, cluster, availabilityZone)) - muxRouter.HandleFunc(v2.TaskMetadataPathWithSlash, v2.TaskContainerMetadataHandler(state, cluster, availabilityZone)) + muxRouter.HandleFunc(v2.ContainerMetadataPath, v2.TaskContainerMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, false)) + muxRouter.HandleFunc(v2.TaskMetadataPath, v2.TaskContainerMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, false)) + muxRouter.HandleFunc(v2.TaskWithTagsMetadataPath, v2.TaskContainerMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, true)) + muxRouter.HandleFunc(v2.TaskMetadataPathWithSlash, v2.TaskContainerMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, false)) + muxRouter.HandleFunc(v2.TaskWithTagsMetadataPathWithSlash, v2.TaskContainerMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, true)) muxRouter.HandleFunc(v2.ContainerStatsPath, v2.TaskContainerStatsHandler(state, statsEngine)) muxRouter.HandleFunc(v2.TaskStatsPath, v2.TaskContainerStatsHandler(state, statsEngine)) muxRouter.HandleFunc(v2.TaskStatsPathWithSlash, v2.TaskContainerStatsHandler(state, statsEngine)) @@ -108,11 +115,14 @@ func v2HandlersSetup(muxRouter *mux.Router, // v3HandlersSetup adds all handlers in v3 package to the mux router. func v3HandlersSetup(muxRouter *mux.Router, state dockerstate.TaskEngineState, + ecsClient api.ECSClient, statsEngine stats.Engine, cluster string, - availabilityZone string) { + availabilityZone string, + containerInstanceArn string) { muxRouter.HandleFunc(v3.ContainerMetadataPath, v3.ContainerMetadataHandler(state)) - muxRouter.HandleFunc(v3.TaskMetadataPath, v3.TaskMetadataHandler(state, cluster, availabilityZone)) + muxRouter.HandleFunc(v3.TaskMetadataPath, v3.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, false)) + muxRouter.HandleFunc(v3.TaskWithTagsMetadataPath, v3.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, true)) muxRouter.HandleFunc(v3.ContainerStatsPath, v3.ContainerStatsHandler(state, statsEngine)) muxRouter.HandleFunc(v3.TaskStatsPath, v3.TaskStatsHandler(state, statsEngine)) } @@ -121,6 +131,7 @@ func v3HandlersSetup(muxRouter *mux.Router, // for tasks being managed by the agent. func ServeTaskHTTPEndpoint(credentialsManager credentials.Manager, state dockerstate.TaskEngineState, + ecsClient api.ECSClient, containerInstanceArn string, cfg *config.Config, statsEngine stats.Engine, @@ -136,8 +147,8 @@ func ServeTaskHTTPEndpoint(credentialsManager credentials.Manager, auditLogger := audit.NewAuditLog(containerInstanceArn, cfg, logger) - server := taskServerSetup(credentialsManager, auditLogger, state, cfg.Cluster, statsEngine, - cfg.TaskMetadataSteadyStateRate, cfg.TaskMetadataBurstRate, availabilityZone) + server := taskServerSetup(credentialsManager, auditLogger, state, ecsClient, cfg.Cluster, statsEngine, + cfg.TaskMetadataSteadyStateRate, cfg.TaskMetadataBurstRate, availabilityZone, containerInstanceArn) for { utils.RetryWithBackoff(utils.NewSimpleBackoff(time.Second, time.Minute, 0.2, 2), func() error { diff --git a/agent/handlers/task_server_setup_test.go b/agent/handlers/task_server_setup_test.go index 10084c564c8..20f28f5ac8a 100644 --- a/agent/handlers/task_server_setup_test.go +++ b/agent/handlers/task_server_setup_test.go @@ -29,12 +29,14 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + mock_api "github.com/aws/amazon-ecs-agent/agent/api/mocks" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/containermetadata" "github.com/aws/amazon-ecs-agent/agent/credentials" mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" "github.com/aws/amazon-ecs-agent/agent/handlers/utils" "github.com/aws/amazon-ecs-agent/agent/handlers/v1" @@ -48,32 +50,34 @@ import ( ) const ( - clusterName = "default" - remoteIP = "169.254.170.3" - remotePort = "32146" - taskARN = "t1" - family = "sleep" - version = "1" - containerID = "cid" - containerName = "sleepy" - imageName = "busybox" - imageID = "bUsYbOx" - cpu = 1024 - memory = 512 - statusRunning = "RUNNING" - containerType = "NORMAL" - containerPort = 80 - containerPortProtocol = "tcp" - eniIPv4Address = "10.0.0.2" - roleArn = "r1" - accessKeyID = "ak" - secretAccessKey = "sk" - credentialsID = "credentialsId" - v2BaseStatsPath = "/v2/stats" - v2BaseMetadataPath = "/v2/metadata" - v3BasePath = "/v3/" - v3EndpointID = "v3eid" - availabilityzone = "us-west-2b" + clusterName = "default" + remoteIP = "169.254.170.3" + remotePort = "32146" + taskARN = "t1" + family = "sleep" + version = "1" + containerID = "cid" + containerName = "sleepy" + imageName = "busybox" + imageID = "bUsYbOx" + cpu = 1024 + memory = 512 + statusRunning = "RUNNING" + containerType = "NORMAL" + containerPort = 80 + containerPortProtocol = "tcp" + eniIPv4Address = "10.0.0.2" + roleArn = "r1" + accessKeyID = "ak" + secretAccessKey = "sk" + credentialsID = "credentialsId" + v2BaseStatsPath = "/v2/stats" + v2BaseMetadataPath = "/v2/metadata" + v2BaseMetadataWithTagsPath = "/v2/metadataWithTags" + v3BasePath = "/v3/" + v3EndpointID = "v3eid" + availabilityzone = "us-west-2b" + containerInstanceArn = "containerInstanceArn-test" ) var ( @@ -312,8 +316,9 @@ func testErrorResponsesFromServer(t *testing.T, path string, expectedErrorMessag credentialsManager := mock_credentials.NewMockManager(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) - server := taskServerSetup(credentialsManager, auditLog, nil, "", nil, config.DefaultTaskMetadataSteadyStateRate, - config.DefaultTaskMetadataBurstRate, "") + ecsClient := mock_api.NewMockECSClient(ctrl) + server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", nil, config.DefaultTaskMetadataSteadyStateRate, + config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", path, nil) @@ -346,8 +351,9 @@ func getResponseForCredentialsRequest(t *testing.T, expectedStatus int, defer ctrl.Finish() credentialsManager := mock_credentials.NewMockManager(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) - server := taskServerSetup(credentialsManager, auditLog, nil, "", nil, config.DefaultTaskMetadataSteadyStateRate, - config.DefaultTaskMetadataBurstRate, "") + ecsClient := mock_api.NewMockECSClient(ctrl) + server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", nil, config.DefaultTaskMetadataSteadyStateRate, + config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() creds, ok := getCredentials() @@ -407,14 +413,15 @@ func TestV2TaskMetadata(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) gomock.InOrder( state.EXPECT().GetTaskByIPAddress(remoteIP).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", tc.path, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -430,6 +437,91 @@ func TestV2TaskMetadata(t *testing.T) { } } +func TestV2TaskWithTagsMetadata(t *testing.T) { + testCases := []struct { + path string + }{ + { + v2BaseMetadataWithTagsPath, + }, + { + v2BaseMetadataWithTagsPath + "/", + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Testing path: %s", tc.path), func(t *testing.T) { + state := mock_dockerstate.NewMockTaskEngineState(ctrl) + auditLog := mock_audit.NewMockAuditLogger(ctrl) + statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) + + expectedTaskResponseWithTags := expectedTaskResponse + expectedContainerInstanceTags := map[string]string{ + "ContainerInstanceTag1": "firstTag", + "ContainerInstanceTag2": "secondTag", + } + expectedTaskResponseWithTags.ContainerInstanceTags = expectedContainerInstanceTags + expectedTaskTags := map[string]string{ + "TaskTag1": "firstTag", + "TaskTag2": "secondTag", + } + expectedTaskResponseWithTags.TaskTags = expectedTaskTags + + contInstTag1Key := "ContainerInstanceTag1" + contInstTag1Val := "firstTag" + contInstTag2Key := "ContainerInstanceTag2" + contInstTag2Val := "secondTag" + taskTag1Key := "TaskTag1" + taskTag1Val := "firstTag" + taskTag2Key := "TaskTag2" + taskTag2Val := "secondTag" + + gomock.InOrder( + state.EXPECT().GetTaskByIPAddress(remoteIP).Return(taskARN, true), + 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, + }, + }, nil), + ) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + recorder := httptest.NewRecorder() + req, _ := http.NewRequest("GET", v2BaseMetadataWithTagsPath, nil) + req.RemoteAddr = remoteIP + ":" + remotePort + 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, expectedTaskResponseWithTags, taskResponse) + }) + } +} + func TestV2ContainerMetadata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -437,14 +529,15 @@ func TestV2ContainerMetadata(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) gomock.InOrder( state.EXPECT().GetTaskByIPAddress(remoteIP).Return(taskARN, true), state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByID(containerID).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v2BaseMetadataPath+"/"+containerID, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -465,14 +558,15 @@ func TestV2ContainerStats(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) dockerStats := &docker.Stats{NumProcs: 2} gomock.InOrder( state.EXPECT().GetTaskByIPAddress(remoteIP).Return(taskARN, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v2BaseStatsPath+"/"+containerID, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -506,6 +600,7 @@ func TestV2TaskStats(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) dockerStats := &docker.Stats{NumProcs: 2} containerMap := map[string]*apicontainer.DockerContainer{ @@ -518,8 +613,8 @@ func TestV2TaskStats(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerMap, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", tc.path, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -544,14 +639,15 @@ func TestV3TaskMetadata(t *testing.T) { 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(task, true), state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone) + 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) @@ -564,6 +660,76 @@ func TestV3TaskMetadata(t *testing.T) { assert.Equal(t, expectedTaskResponse, taskResponse) } +// Test API calls for propagating Tags to Task Metadata +func TestV3TaskMetadataWithTags(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) + + expectedTaskResponseWithTags := expectedTaskResponse + expectedContainerInstanceTags := map[string]string{ + "ContainerInstanceTag1": "firstTag", + "ContainerInstanceTag2": "secondTag", + } + expectedTaskResponseWithTags.ContainerInstanceTags = expectedContainerInstanceTags + expectedTaskTags := map[string]string{ + "TaskTag1": "firstTag", + "TaskTag2": "secondTag", + } + expectedTaskResponseWithTags.TaskTags = expectedTaskTags + + contInstTag1Key := "ContainerInstanceTag1" + contInstTag1Val := "firstTag" + contInstTag2Key := "ContainerInstanceTag2" + contInstTag2Val := "secondTag" + taskTag1Key := "TaskTag1" + taskTag1Val := "firstTag" + taskTag2Key := "TaskTag2" + taskTag2Val := "secondTag" + + gomock.InOrder( + state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), + 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, + }, + }, nil), + ) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + recorder := httptest.NewRecorder() + req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/taskWithTags", 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, expectedTaskResponseWithTags, taskResponse) +} + func TestV3ContainerMetadata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -571,14 +737,15 @@ func TestV3ContainerMetadata(t *testing.T) { 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(dockerContainer, true), state.EXPECT().TaskByID(containerID).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + 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) @@ -598,6 +765,7 @@ func TestV3TaskStats(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) dockerStats := &docker.Stats{NumProcs: 2} @@ -612,8 +780,8 @@ func TestV3TaskStats(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerMap, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/task/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -635,6 +803,7 @@ func TestV3ContainerStats(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) dockerStats := &docker.Stats{NumProcs: 2} @@ -643,8 +812,8 @@ func TestV3ContainerStats(t *testing.T) { state.EXPECT().DockerIDByV3EndpointID(v3EndpointID).Return(containerID, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -677,9 +846,10 @@ func TestTaskHTTPEndpointErrorCode404(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) for _, testPath := range testPaths { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { @@ -717,9 +887,10 @@ func TestTaskHTTPEndpointErrorCode400(t *testing.T) { state := mock_dockerstate.NewMockTaskEngineState(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "") + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) for _, testPath := range testPaths { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { diff --git a/agent/handlers/v2/response.go b/agent/handlers/v2/response.go index 3ebca165edd..61e36512292 100644 --- a/agent/handlers/v2/response.go +++ b/agent/handlers/v2/response.go @@ -16,6 +16,7 @@ package v2 import ( "time" + "github.com/aws/amazon-ecs-agent/agent/api" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" "github.com/aws/amazon-ecs-agent/agent/containermetadata" @@ -29,18 +30,20 @@ import ( // 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"` + 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"` } // ContainerResponse defines the schema for the container response @@ -76,8 +79,11 @@ type LimitsResponse struct { // NewTaskResponse creates a new response object for the task func NewTaskResponse(taskARN string, state dockerstate.TaskEngineState, + ecsClient api.ECSClient, cluster string, - az string) (*TaskResponse, error) { + az string, + containerInstanceArn string, + propagateTags bool) (*TaskResponse, error) { task, ok := state.TaskByArn(taskARN) if !ok { return nil, errors.Errorf("v2 task response: unable to find task '%s'", taskARN) @@ -128,9 +134,35 @@ func NewTaskResponse(taskARN string, resp.Containers = append(resp.Containers, containerResponse) } + if propagateTags { + propagateTagsToMetadata(state, ecsClient, containerInstanceArn, taskARN, resp) + } + return resp, nil } +func propagateTagsToMetadata(state dockerstate.TaskEngineState, ecsClient api.ECSClient, containerInstanceArn, taskARN string, resp *TaskResponse) { + containerInstanceTags, err := ecsClient.GetResourceTags(containerInstanceArn) + if err == nil { + resp.ContainerInstanceTags = make(map[string]string) + for _, tag := range containerInstanceTags { + resp.ContainerInstanceTags[*tag.Key] = *tag.Value + } + } else { + seelog.Errorf("Could not get container instance tags for %s: %s", containerInstanceArn, err.Error()) + } + + taskTags, err := ecsClient.GetResourceTags(taskARN) + if err == nil { + resp.TaskTags = make(map[string]string) + for _, tag := range taskTags { + resp.TaskTags[*tag.Key] = *tag.Value + } + } else { + seelog.Errorf("Could not get task tags for %s: %s", taskARN, err.Error()) + } +} + // NewContainerResponse creates a new container response based on container id func NewContainerResponse(containerID string, state dockerstate.TaskEngineState) (*ContainerResponse, error) { diff --git a/agent/handlers/v2/response_test.go b/agent/handlers/v2/response_test.go index a36399bf260..272e4a87836 100644 --- a/agent/handlers/v2/response_test.go +++ b/agent/handlers/v2/response_test.go @@ -24,8 +24,10 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + mock_api "github.com/aws/amazon-ecs-agent/agent/api/mocks" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" "github.com/aws/aws-sdk-go/aws" docker "github.com/fsouza/go-dockerclient" @@ -34,21 +36,22 @@ import ( ) const ( - taskARN = "t1" - cluster = "default" - family = "sleep" - version = "1" - containerID = "cid" - containerName = "sleepy" - imageName = "busybox" - imageID = "bUsYbOx" - cpu = 1024 - memory = 512 - eniIPv4Address = "10.0.0.2" - volName = "volume1" - volSource = "/var/lib/volume1" - volDestination = "/volume" - availabilityZone = "us-west-2b" + taskARN = "t1" + cluster = "default" + family = "sleep" + version = "1" + containerID = "cid" + containerName = "sleepy" + imageName = "busybox" + imageID = "bUsYbOx" + cpu = 1024 + memory = 512 + eniIPv4Address = "10.0.0.2" + volName = "volume1" + volSource = "/var/lib/volume1" + volDestination = "/volume" + availabilityZone = "us-west-2b" + containerInstanceArn = "containerInstance-test" ) func TestTaskResponse(t *testing.T) { @@ -56,6 +59,7 @@ func TestTaskResponse(t *testing.T) { defer ctrl.Finish() state := mock_dockerstate.NewMockTaskEngineState(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) now := time.Now() task := &apitask.Task{ Arn: taskARN, @@ -117,7 +121,7 @@ func TestTaskResponse(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), ) - taskResponse, err := NewTaskResponse(taskARN, state, cluster, availabilityZone) + taskResponse, err := NewTaskResponse(taskARN, state, ecsClient, cluster, availabilityZone, containerInstanceArn, false) assert.NoError(t, err) _, err = json.Marshal(taskResponse) assert.NoError(t, err) @@ -251,9 +255,18 @@ func TestTaskResponseMarshal(t *testing.T) { }, }, }, + "ContainerInstanceTags": map[string]interface{}{ + "ContainerInstanceTag1": "firstTag", + "ContainerInstanceTag2": "secondTag", + }, + "TaskTags": map[string]interface{}{ + "TaskTag1": "firstTag", + "TaskTag2": "secondTag", + }, } state := mock_dockerstate.NewMockTaskEngineState(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) task := &apitask.Task{ Arn: taskARN, @@ -291,12 +304,41 @@ func TestTaskResponseMarshal(t *testing.T) { }, } + contInstTag1Key := "ContainerInstanceTag1" + contInstTag1Val := "firstTag" + contInstTag2Key := "ContainerInstanceTag2" + contInstTag2Val := "secondTag" + taskTag1Key := "TaskTag1" + taskTag1Val := "firstTag" + taskTag2Key := "TaskTag2" + taskTag2Val := "secondTag" + gomock.InOrder( 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, + }, + }, nil), ) - taskResponse, err := NewTaskResponse(taskARN, state, cluster, availabilityZone) + taskResponse, err := NewTaskResponse(taskARN, state, ecsClient, cluster, availabilityZone, containerInstanceArn, true) assert.NoError(t, err) taskResponseJSON, err := json.Marshal(taskResponse) diff --git a/agent/handlers/v2/task_container_metadata_handler.go b/agent/handlers/v2/task_container_metadata_handler.go index 23d3afdc01f..726c0432de3 100644 --- a/agent/handlers/v2/task_container_metadata_handler.go +++ b/agent/handlers/v2/task_container_metadata_handler.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" + "github.com/aws/amazon-ecs-agent/agent/api" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/handlers/utils" "github.com/cihub/seelog" @@ -31,15 +32,21 @@ const ( // TaskMetadataPath specifies the relative URI path for serving task metadata. TaskMetadataPath = "/v2/metadata" + // TaskWithTagsMetadataPath specifies the relative URI path for serving task metadata with Container Instance and Task Tags. + TaskWithTagsMetadataPath = "/v2/metadataWithTags" + // TaskMetadataPathWithSlash specifies the relative URI path for serving task metadata. TaskMetadataPathWithSlash = TaskMetadataPath + "/" + + // TaskWithTagsMetadataPath specifies the relative URI path for serving task metadata with Container Instance and Task Tags. + TaskWithTagsMetadataPathWithSlash = TaskWithTagsMetadataPath + "/" ) // ContainerMetadataPath specifies the relative URI path for serving container metadata. var ContainerMetadataPath = TaskMetadataPathWithSlash + utils.ConstructMuxVar(metadataContainerIDMuxName, utils.AnythingButEmptyRegEx) // TaskContainerMetadataHandler returns the handler method for handling task and container metadata requests. -func TaskContainerMetadataHandler(state dockerstate.TaskEngineState, cluster string, az string) func(http.ResponseWriter, *http.Request) { +func TaskContainerMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSClient, cluster, az, containerInstanceArn string, propagateTags bool) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { taskARN, err := getTaskARNByRequest(r, state) if err != nil { @@ -55,7 +62,7 @@ func TaskContainerMetadataHandler(state dockerstate.TaskEngineState, cluster str } seelog.Infof("V2 task/container metadata handler: writing response for task '%s'", taskARN) - WriteTaskMetadataResponse(w, taskARN, cluster, state, az) + WriteTaskMetadataResponse(w, taskARN, cluster, state, ecsClient, az, containerInstanceArn, propagateTags) } } @@ -73,9 +80,9 @@ func WriteContainerMetadataResponse(w http.ResponseWriter, containerID string, s } // WriteTaskMetadataResponse writes the task metadata to response writer. -func WriteTaskMetadataResponse(w http.ResponseWriter, taskARN string, cluster string, state dockerstate.TaskEngineState, az string) { +func WriteTaskMetadataResponse(w http.ResponseWriter, taskARN string, cluster string, state dockerstate.TaskEngineState, ecsClient api.ECSClient, az, containerInstanceArn string, propagateTags bool) { // Generate a response for the task - taskResponse, err := NewTaskResponse(taskARN, state, cluster, az) + taskResponse, err := 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) diff --git a/agent/handlers/v3/task_metadata_handler.go b/agent/handlers/v3/task_metadata_handler.go index f9151690f28..8f8f6786950 100644 --- a/agent/handlers/v3/task_metadata_handler.go +++ b/agent/handlers/v3/task_metadata_handler.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" + "github.com/aws/amazon-ecs-agent/agent/api" "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" @@ -30,8 +31,12 @@ const v3EndpointIDMuxName = "v3EndpointIDMuxName" // TaskMetadataPath specifies the relative URI path for serving task metadata. var TaskMetadataPath = "/v3/" + utils.ConstructMuxVar(v3EndpointIDMuxName, utils.AnythingButSlashRegEx) + "/task" +// TaskWithTagsMetadataPath specifies the relative URI path for serving task metdata +// with Container Instance and Task Tags retrieved through the ECS API +var TaskWithTagsMetadataPath = "/v3/" + utils.ConstructMuxVar(v3EndpointIDMuxName, utils.AnythingButSlashRegEx) + "/taskWithTags" + // TaskMetadataHandler returns the handler method for handling task metadata requests. -func TaskMetadataHandler(state dockerstate.TaskEngineState, cluster string, az string) func(http.ResponseWriter, *http.Request) { +func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSClient, cluster, az, containerInstanceArn string, propagateTags bool) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { taskARN, err := getTaskARNByRequest(r, state) if err != nil { @@ -44,6 +49,6 @@ func TaskMetadataHandler(state dockerstate.TaskEngineState, cluster string, az s 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, az) + v2.WriteTaskMetadataResponse(w, taskARN, cluster, state, ecsClient, az, containerInstanceArn, propagateTags) } } diff --git a/misc/taskmetadata-validator/taskmetadata-validator.go b/misc/taskmetadata-validator/taskmetadata-validator.go index dffbffd135f..bae5eecc740 100644 --- a/misc/taskmetadata-validator/taskmetadata-validator.go +++ b/misc/taskmetadata-validator/taskmetadata-validator.go @@ -25,23 +25,26 @@ import ( ) const ( - v2MetadataEndpoint = "http://169.254.170.2/v2/metadata" - v2StatsEndpoint = "http://169.254.170.2/v2/stats" - maxRetries = 4 - durationBetweenRetries = time.Second + v2MetadataEndpoint = "http://169.254.170.2/v2/metadata" + v2MetadataWithTagsEndpoint = "http://169.254.170.2/v2/metadataWithTags" + v2StatsEndpoint = "http://169.254.170.2/v2/stats" + maxRetries = 4 + durationBetweenRetries = time.Second ) // TaskResponse defines the schema for the task response JSON object type TaskResponse struct { - Cluster string - TaskARN string - Family string - Revision string - DesiredStatus string `json:",omitempty"` - KnownStatus string - AvailabilityZone string - Containers []ContainerResponse `json:",omitempty"` - Limits LimitsResponse `json:",omitempty"` + Cluster string + TaskARN string + Family string + Revision string + DesiredStatus string `json:",omitempty"` + KnownStatus string + AvailabilityZone string + Containers []ContainerResponse `json:",omitempty"` + Limits LimitsResponse `json:",omitempty"` + TaskTags map[string]string `json:"TaskTags,omitempty"` + ContainerInstanceTags map[string]string `json:"ContainerInstanceTags,omitempty"` } // ContainerResponse defines the schema for the container response @@ -120,6 +123,23 @@ func taskMetadata(client *http.Client) (*TaskResponse, error) { return &taskMetadata, nil } +func taskWithTagsMetadata(client *http.Client) (*TaskResponse, error) { + body, err := metadataResponse(client, v2MetadataWithTagsEndpoint, "task with tags metadata") + if err != nil { + return nil, err + } + + fmt.Printf("Received task with tags metadata: %s \n", string(body)) + + var taskMetadata TaskResponse + err = json.Unmarshal(body, &taskMetadata) + if err != nil { + return nil, fmt.Errorf("task with tags metadata: unable to parse response body: %v", err) + } + + return &taskMetadata, nil +} + func containerMetadata(client *http.Client, id string) (*ContainerResponse, error) { body, err := metadataResponse(client, v2MetadataEndpoint+"/"+id, "container metadata") if err != nil { @@ -214,6 +234,21 @@ func main() { // Wait for the Health information to be ready time.Sleep(5 * time.Second) + // If the image is built with option to check Tags + argsWithoutProg := os.Args[1:] + if len(argsWithoutProg) > 0 && argsWithoutProg[0] == "CheckTags" { + taskWithTagsMetadata, err := taskWithTagsMetadata(client) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to get task stats: %v", err) + os.Exit(1) + } + + if len(taskWithTagsMetadata.ContainerInstanceTags) == 0 { + fmt.Fprintf(os.Stderr, "ContainerInstanceTags not found: %v", err) + os.Exit(1) + } + } + taskMetadata, err := taskMetadata(client) if err != nil { fmt.Fprintf(os.Stderr, "Unable to get task metadata: %v", err) diff --git a/misc/v3-task-endpoint-validator-windows/v3-task-endpoint-validator-windows.go b/misc/v3-task-endpoint-validator-windows/v3-task-endpoint-validator-windows.go index d735b6a7883..463ba170fe1 100644 --- a/misc/v3-task-endpoint-validator-windows/v3-task-endpoint-validator-windows.go +++ b/misc/v3-task-endpoint-validator-windows/v3-task-endpoint-validator-windows.go @@ -28,6 +28,8 @@ const ( durationBetweenRetries = time.Second ) +var checkContainerInstanceTags bool + // TaskResponse defines the schema for the task response JSON object type TaskResponse struct { Cluster string `json:"Cluster"` @@ -169,6 +171,9 @@ func verifyTaskMetadataResponse(taskMetadataRawMsg json.RawMessage) error { } taskExpectedFieldNotEmptyArray := []string{"Cluster", "TaskARN", "Family", "Revision", "PullStartedAt", "PullStoppedAt", "Containers", "AvailabilityZone"} + if checkContainerInstanceTags { + taskExpectedFieldNotEmptyArray = append(taskExpectedFieldNotEmptyArray, "ContainerInstanceTags") + } for fieldName, fieldVal := range taskExpectedFieldEqualMap { if err = fieldEqual(taskMetadataResponseMap, fieldName, fieldVal); err != nil { @@ -309,12 +314,25 @@ func main() { Timeout: 5 * time.Second, } + // If the image is built with option to check Tags + argsWithoutProg := os.Args[1:] + if len(argsWithoutProg) > 0 { + if argsWithoutProg[0] == "CheckTags" { + checkContainerInstanceTags = true + } + } + // Wait for the Health information to be ready time.Sleep(5 * time.Second) v3BaseEndpoint := os.Getenv(containerMetadataEnvVar) containerMetadataPath := v3BaseEndpoint - taskMetadataPath := v3BaseEndpoint + "/task" + taskMetadataPath := v3BaseEndpoint + if checkContainerInstanceTags { + taskMetadataPath += "/taskWithTags" + } else { + taskMetadataPath += "/task" + } containerStatsPath := v3BaseEndpoint + "/stats" taskStatsPath := v3BaseEndpoint + "/task/stats" diff --git a/misc/v3-task-endpoint-validator/Dockerfile b/misc/v3-task-endpoint-validator/Dockerfile index d7c13ed8226..dc163f7f3e2 100644 --- a/misc/v3-task-endpoint-validator/Dockerfile +++ b/misc/v3-task-endpoint-validator/Dockerfile @@ -14,5 +14,4 @@ FROM busybox:1.29.3 MAINTAINER Amazon Web Services, Inc. COPY v3-task-endpoint-validator / - ENTRYPOINT ["/v3-task-endpoint-validator"] 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 120adcc3da1..11da76d7386 100644 --- a/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go +++ b/misc/v3-task-endpoint-validator/v3-task-endpoint-validator.go @@ -31,6 +31,7 @@ const ( ) var isAWSVPCNetworkMode bool +var checkContainerInstanceTags bool // TaskResponse defines the schema for the task response JSON object type TaskResponse struct { @@ -185,6 +186,9 @@ func verifyTaskMetadataResponse(taskMetadataRawMsg json.RawMessage) error { } taskExpectedFieldNotEmptyArray := []string{"Cluster", "TaskARN", "Family", "Revision", "PullStartedAt", "PullStoppedAt", "Containers", "AvailabilityZone"} + if checkContainerInstanceTags { + taskExpectedFieldNotEmptyArray = append(taskExpectedFieldNotEmptyArray, "ContainerInstanceTags") + } for fieldName, fieldVal := range taskExpectedFieldEqualMap { if err = fieldEqual(taskMetadataResponseMap, fieldName, fieldVal); err != nil { @@ -411,13 +415,26 @@ func main() { Timeout: 5 * time.Second, } + // If the image is built with option to check Tags + argsWithoutProg := os.Args[1:] + if len(argsWithoutProg) > 0 { + if argsWithoutProg[0] == "CheckTags" { + checkContainerInstanceTags = true + } + } + // Wait for the Health information to be ready time.Sleep(5 * time.Second) isAWSVPCNetworkMode = false v3BaseEndpoint := os.Getenv(containerMetadataEnvVar) containerMetadataPath := v3BaseEndpoint - taskMetadataPath := v3BaseEndpoint + "/task" + taskMetadataPath := v3BaseEndpoint + if checkContainerInstanceTags { + taskMetadataPath += "/taskWithTags" + } else { + taskMetadataPath += "/task" + } containerStatsPath := v3BaseEndpoint + "/stats" taskStatsPath := v3BaseEndpoint + "/task/stats"