diff --git a/README.md b/README.md index d336bc9f40d..d9d0fd0dfee 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ Additionally, the following environment variable(s) can be used to configure the | `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | <true | false> | By default, the ecs-init service adds an iptable rule to block access to ECS Agent's introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` is set to true, this rule will not be added/removed. | false | | `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is `eth0` | `eth0` | | `ECS_AGENT_LABELS` | `{"test.label.1":"value1","test.label.2":"value2"}` | The labels to add to the ECS Agent container. | | +| `ECS_PAUSE_LABELS` | `{"test.pause.label.1":"value1","test.pause.label.2":"value2"}` | The labels to add to the pause container. | | | `ECS_AGENT_APPARMOR_PROFILE` | `unconfined` | Specifies the name of the AppArmor profile to run the ecs-agent container under. This only applies to AppArmor-enabled systems, such as Ubuntu, Debian, and SUSE. If unset, defaults to the profile written out by ecs-init (ecs-agent-default). | `ecs-agent-default` | | `ECS_AGENT_PID_NAMESPACE_HOST` | <true | false> | By default, the ECS agent container runs with its own PID namespace. If ECS_AGENT_PID_NAMESPACE_HOST is set to true, ecs-init will start the ECS agent container with the host's PID namespace. This is particularly useful when running on SELinux-enforcing hosts with Docker's SELinux option enabled. | false | diff --git a/agent/api/task/task.go b/agent/api/task/task.go index 2a20ce269aa..f3a283d9b98 100644 --- a/agent/api/task/task.go +++ b/agent/api/task/task.go @@ -17,6 +17,7 @@ import ( "context" "encoding/json" "fmt" + "os" "path/filepath" "reflect" "strconv" @@ -55,6 +56,7 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-agent/utils/arn" "github.com/aws/amazon-ecs-agent/ecs-agent/utils/ttime" ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + log "github.com/cihub/seelog" "github.com/aws/aws-sdk-go-v2/aws" "github.com/docker/docker/api/types" @@ -163,6 +165,8 @@ const ( serviceConnectAttachmentType = "serviceconnectdetail" ipv6LoopbackAddress = "::1" + + PAUSE_LABELS_ENV_VAR = "ECS_PAUSE_LABELS" ) // TaskOverrides are the overrides applied to a task @@ -1811,6 +1815,14 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc containerConfig.Labels = make(map[string]string) } + switch container.Type { + case apicontainer.ContainerCNIPause, apicontainer.ContainerNamespacePause: + if pauseLabels := os.Getenv(PAUSE_LABELS_ENV_VAR); pauseLabels != "" { + // Set labels to pause container if it's provieded as env var. + setLabelsFromJsonString(containerConfig, pauseLabels) + } + } + if container.Type == apicontainer.ContainerCNIPause && task.IsNetworkModeAWSVPC() { // apply hostname to pause container's docker config return task.applyENIHostname(containerConfig), nil @@ -1819,6 +1831,26 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc return containerConfig, nil } +// Parse label string and set them to the given container configuration. +func setLabelsFromJsonString(config *dockercontainer.Config, labelsString string) { + if len(labelsString) > 0 { + labels, err := toLabelMap(labelsString) + if err != nil { + log.Errorf("Skipped setting labels because of failed to decode. Error: %s", err) + return + } + if len(labels) > 0 { + config.Labels = labels + } + } +} + +func toLabelMap(jsonBlock string) (map[string]string, error) { + out := map[string]string{} + err := json.Unmarshal([]byte(jsonBlock), &out) + return out, err +} + // dockerExposedPorts returns the container ports that need to be exposed for a container // 1. For bridge-mode ServiceConnect-enabled tasks: // 1a. Pause containers need to expose the port(s) for their associated task container. In particular, SC pause container diff --git a/agent/api/task/task_test.go b/agent/api/task/task_test.go index ba8ccb2a2a0..de4b7ae591d 100644 --- a/agent/api/task/task_test.go +++ b/agent/api/task/task_test.go @@ -5924,3 +5924,120 @@ func TestGenerateENIExtraHosts(t *testing.T) { }) } } + +func TestDockerConfigPauseContainerLabelsWithoutEnvVar_ECS_PAUSE_LABELS(t *testing.T) { + testTask := &Task{ + Arn: "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe", + Family: "myFamily", + Version: "1", + Containers: []*apicontainer.Container{ + { + Name: "pause", + Type: apicontainer.ContainerCNIPause, + }, + }, + } + + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if configErr != nil { + t.Fatal(configErr) + } + + assert.Equal(t, 0, len(config.Labels)) +} + +func TestDockerConfigPauseContainerLabelsWithEnvVar_ECS_PAUSE_LABELS(t *testing.T) { + testTask := &Task{ + Arn: "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe", + Family: "myFamily", + Version: "1", + Containers: []*apicontainer.Container{ + { + Name: "pause", + Type: apicontainer.ContainerCNIPause, + }, + }, + } + labelsString := "{\"test.label.1\":\"test_a\",\"test.label.2\":\"test_b\"}" + t.Setenv("ECS_PAUSE_LABELS", labelsString) + + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if configErr != nil { + t.Fatal(configErr) + } + + assert.Equal(t, 2, len(config.Labels)) + assert.Equal(t, "test_a", config.Labels["test.label.1"]) + assert.Equal(t, "test_b", config.Labels["test.label.2"]) +} + +func TestDockerConfigNamespacePauseContainerLabelsWithEnvVar_ECS_PAUSE_LABELS(t *testing.T) { + testTask := &Task{ + Arn: "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe", + Family: "myFamily", + Version: "1", + Containers: []*apicontainer.Container{ + { + Name: "pause", + Type: apicontainer.ContainerNamespacePause, + }, + }, + } + labelsString := "{\"test.label.1\":\"test_a\",\"test.label.2\":\"test_b\"}" + t.Setenv("ECS_PAUSE_LABELS", labelsString) + + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if configErr != nil { + t.Fatal(configErr) + } + + assert.Equal(t, 2, len(config.Labels)) + assert.Equal(t, "test_a", config.Labels["test.label.1"]) + assert.Equal(t, "test_b", config.Labels["test.label.2"]) +} + +func TestDockerConfigPauseContainerLabelsWithInvalidEnvVar_ECS_PAUSE_LABELS(t *testing.T) { + testTask := &Task{ + Arn: "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe", + Family: "myFamily", + Version: "1", + Containers: []*apicontainer.Container{ + { + Name: "pause", + Type: apicontainer.ContainerCNIPause, + }, + }, + } + // Invalid format. + labelsString := "{\"test.label\":\"test\"" + t.Setenv("ECS_PAUSE_LABELS", labelsString) + + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if configErr != nil { + t.Fatal(configErr) + } + + assert.Equal(t, 0, len(config.Labels)) +} + +func TestDockerConfigContainerLabelsWithEnvVar_ECS_PAUSE_LABELS(t *testing.T) { + testTask := &Task{ + Arn: "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe", + Family: "myFamily", + Version: "1", + Containers: []*apicontainer.Container{ + { + Name: "c1", + CPU: uint(10), + Memory: uint(256), + }, + }, + } + + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if configErr != nil { + t.Fatal(configErr) + } + + assert.Equal(t, 0, len(config.Labels)) +}