Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion agent/config/ipcompatibility/ipcompatibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ func NewIPCompatibility(ipv4Compatible, ipv6Compatible bool) IPCompatibility {
return IPCompatibility{ipv4Compatible: ipv4Compatible, ipv6Compatible: ipv6Compatible}
}

// Returns an IPv4-only IPCompatibility instance.
// Returns an IPv4-only IPCompatibility value.
func NewIPv4OnlyCompatibility() IPCompatibility {
return NewIPCompatibility(true, false)
}

// Returns an IPv6-only IPCompatibility value.
func NewIPv6OnlyCompatibility() IPCompatibility {
return NewIPCompatibility(false, true)
}

// IsIPv4Compatible returns the current IPv4 compatibility status.
func (ic *IPCompatibility) IsIPv4Compatible() bool {
return ic.ipv4Compatible
Expand Down
6 changes: 6 additions & 0 deletions agent/config/ipcompatibility/ipcompatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func TestIPv4OnlyCompatibility(t *testing.T) {
assert.False(t, c.IsIPv6Compatible())
}

func TestIPv6OnlyCompatibility(t *testing.T) {
c := NewIPv6OnlyCompatibility()
assert.True(t, c.IsIPv6Compatible())
assert.False(t, c.IsIPv4Compatible())
}

func TestIsIPv6Only(t *testing.T) {
tests := []struct {
name string
Expand Down
146 changes: 126 additions & 20 deletions agent/engine/docker_task_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ import (
"github.com/aws/amazon-ecs-agent/ecs-agent/logger/field"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/retry"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/ttime"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/aws"
ep "github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/smithy-go/ptr"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/registry"
Expand Down Expand Up @@ -89,6 +91,8 @@ const (
logDriverTypeFirelens = "awsfirelens"
logDriverTypeFluentd = "fluentd"
logDriverTypeAwslogs = "awslogs"
awsLogsEndpointKey = "awslogs-endpoint"
awsLogsRegionKey = "awslogs-region"
logDriverTag = "tag"
logDriverMode = "mode"
logDriverBufferSize = "max-buffer-size"
Expand Down Expand Up @@ -1945,29 +1949,33 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a

// This is a short term solution only for specific regions
if hostConfig.LogConfig.Type == logDriverTypeAwslogs {
region := engine.cfg.AWSRegion
if _, ok := unresolvedIsolatedRegions[region]; ok {
endpoint := ""
dnsSuffix := ""
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
if !ok {
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
"region": region,
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
})
dnsSuffix = ep.AwsPartition().DNSSuffix()
} else {
resolvedEndpoint, err := partition.EndpointFor("logs", region)
if err == nil {
endpoint = resolvedEndpoint.URL
if engine.cfg.InstanceIPCompatibility.IsIPv6Only() {
engine.setAWSLogsDualStackEndpoint(task, container, hostConfig)
} else {
region := engine.cfg.AWSRegion
if _, ok := unresolvedIsolatedRegions[region]; ok {
endpoint := ""
dnsSuffix := ""
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
if !ok {
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
"region": region,
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
})
dnsSuffix = ep.AwsPartition().DNSSuffix()
} else {
dnsSuffix = partition.DNSSuffix()
resolvedEndpoint, err := partition.EndpointFor("logs", region)
if err == nil {
endpoint = resolvedEndpoint.URL
} else {
dnsSuffix = partition.DNSSuffix()
}
}
if endpoint == "" {
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
}
hostConfig.LogConfig.Config["awslogs-endpoint"] = endpoint
Comment thread
TheanLim marked this conversation as resolved.
Outdated
}
if endpoint == "" {
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
}
hostConfig.LogConfig.Config["awslogs-endpoint"] = endpoint
}
}

Expand Down Expand Up @@ -2946,3 +2954,101 @@ func (engine *DockerTaskEngine) getDockerID(task *apitask.Task, container *apico
}
return dockerContainer.DockerID, nil
}

// Sets CloudWatch Logs dual stack endpoint as "awslogs-endpoint" option in the logging config.
// This is needed because awslogs driver that we consume from Docker does not support
// an option to enable dual stack endpoints, so customers have no way to enable dual stack endpoints
// that are needed in an IPv6-only environment.
func (engine *DockerTaskEngine) setAWSLogsDualStackEndpoint(
task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig,
) {
// Helper function to populate common logger.Fields
withAdditionalLoggerFields := func(additionalFields logger.Fields) logger.Fields {
fields := logger.Fields{field.TaskARN: task.Arn, field.ContainerName: container.Name}
for k, v := range additionalFields {
fields[k] = v
}
return fields
}

// Do nothing if endpoint is already set
if hostConfig.LogConfig.Config[awsLogsEndpointKey] != "" {
Comment thread
amogh09 marked this conversation as resolved.
logger.Info(
fmt.Sprintf(
"%s is already set in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
awsLogsEndpointKey),
withAdditionalLoggerFields(logger.Fields{}),
)
return
}

// Region is required to resolve endpoint
region := hostConfig.LogConfig.Config[awsLogsRegionKey]
if region == "" {
logger.Warn(
fmt.Sprintf(
"%s not found in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
awsLogsRegionKey),
withAdditionalLoggerFields(logger.Fields{}),
)
return
}

// Docker versions older than 18.09.0 do not support awslogs-endpoint
// option. So, skip endpoint resolution for those Docker versions.
dockerVersion, err := engine.Version()
if err != nil {
logger.Error("Failed to get Docker engine version. Skip resolving dual stack CloudWatch Logs endpoint.",
withAdditionalLoggerFields(logger.Fields{field.Error: err}))
return
}
const thresholdVersion = "18.09.0"
dockerVersionIsCompatible, err := utils.Version(dockerVersion).Matches(">=" + thresholdVersion)
if err != nil {
logger.Error("Failed to determine if docker version is high enough",
withAdditionalLoggerFields(logger.Fields{
field.Error: err,
field.DockerVersion: dockerVersion,
"thresholdVersion": thresholdVersion,
}))
return
}
if !dockerVersionIsCompatible {
logger.Warn(
fmt.Sprintf(
"Docker version does not support %s option. Skip resolving dual stack CloudWatch Logs endpoint.",
awsLogsEndpointKey),
withAdditionalLoggerFields(logger.Fields{}),
Comment thread
amogh09 marked this conversation as resolved.
Outdated
)
return
}

// Resolve the endpoint
endpoint, err := getAWSLogsDualStackEndpoint(region)
Comment thread
xxx0624 marked this conversation as resolved.
if err != nil {
logger.Error(
"Failed to get CloudWatch Logs dual stack endpoint. Skipping setting it.",
withAdditionalLoggerFields(logger.Fields{field.Region: region, field.Error: err}))
return
}

logger.Info("Resolved CloudWatch Logs dual stack endpoint",
withAdditionalLoggerFields(logger.Fields{
field.Endpoint: endpoint,
field.Region: region,
}))
hostConfig.LogConfig.Config[awsLogsEndpointKey] = endpoint
}

// Returns CloudWatch Logs dual stack endpoint for the given region.
func getAWSLogsDualStackEndpoint(region string) (string, error) {
endpoint, err := cloudwatchlogs.NewDefaultEndpointResolverV2().ResolveEndpoint(context.TODO(),
cloudwatchlogs.EndpointParameters{
UseDualStack: ptr.Bool(true),
Region: ptr.String(region),
})
if err != nil {
return "", fmt.Errorf("failed to resolve CloudWatch Logs endpoint for region '%s': %w", region, err)
Comment thread
TheanLim marked this conversation as resolved.
Outdated
}
return endpoint.URI.String(), nil
}
Loading