Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
149 changes: 129 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[awsLogsEndpointKey] = endpoint
}
if endpoint == "" {
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
}
hostConfig.LogConfig.Config["awslogs-endpoint"] = endpoint
}
}

Expand Down Expand Up @@ -2946,3 +2954,104 @@ 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{
field.DockerVersion: dockerVersion,
"thresholdVersion": thresholdVersion,
}),
)
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 dual stack CloudWatch Logs endpoint for region '%s': %w", region, err)
}
return endpoint.URI.String(), nil
}
Loading