Skip to content

Commit d23de2f

Browse files
authored
Merge branch 'dev' into thean/s3-ipv6-client-flag
2 parents 5b100d3 + e805e6b commit d23de2f

136 files changed

Lines changed: 63365 additions & 48 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent/config/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ func NewConfig(ec2client ec2.EC2MetadataClient) (*Config, error) {
240240
ec2client = ec2.NewBlackholeEC2MetadataClient()
241241
}
242242

243-
config.determineIPCompatibility(ec2client)
243+
// TODO feat:IPv6-only - Enable when launching IPv6-only support
244+
// config.determineIPCompatibility(ec2client)
244245

245246
if config.complete() {
246247
// No need to do file / network IO

agent/config/config_unix.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ func getConfigFileName() (string, error) {
170170
// determining the IP compatibility of the container instance.
171171
// This is a fallback to help with graceful adoption of Agent in IPv6-only environments
172172
// without disrupting existing environments.
173+
//
174+
// TODO feat:IPv6-only - Remove lint rule below
175+
//
176+
//lint:ignore U1000 Function will be used in the future
173177
func (c *Config) determineIPCompatibility(ec2client ec2.EC2MetadataClient) {
174178
// Load primary ENI's MAC address on EC2 Launch Type
175179
var primaryENIMAC string

agent/config/config_unix_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@ func TestDetermineIPCompatibility(t *testing.T) {
384384
// Tests that IPCompatibility defaults to IPv4-only when determining IP compatibility of
385385
// the container instance fails due to some error.
386386
func TestIPCompatibilityFallback(t *testing.T) {
387+
// TODO feat:IPv6-only - Remove skip
388+
t.Skip("Enable when launching IPv6-only support")
387389
defer setTestRegion()()
388390
ctrl := gomock.NewController(t)
389391
mockEc2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl)

agent/config/ipcompatibility/ipcompatibility.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ func NewIPCompatibility(ipv4Compatible, ipv6Compatible bool) IPCompatibility {
2424
return IPCompatibility{ipv4Compatible: ipv4Compatible, ipv6Compatible: ipv6Compatible}
2525
}
2626

27-
// Returns an IPv4-only IPCompatibility instance.
27+
// Returns an IPv4-only IPCompatibility value.
2828
func NewIPv4OnlyCompatibility() IPCompatibility {
2929
return NewIPCompatibility(true, false)
3030
}
3131

32+
// Returns an IPv6-only IPCompatibility value.
33+
func NewIPv6OnlyCompatibility() IPCompatibility {
34+
return NewIPCompatibility(false, true)
35+
}
36+
3237
// IsIPv4Compatible returns the current IPv4 compatibility status.
3338
func (ic *IPCompatibility) IsIPv4Compatible() bool {
3439
return ic.ipv4Compatible

agent/config/ipcompatibility/ipcompatibility_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ func TestIPv4OnlyCompatibility(t *testing.T) {
4444
assert.False(t, c.IsIPv6Compatible())
4545
}
4646

47+
func TestIPv6OnlyCompatibility(t *testing.T) {
48+
c := NewIPv6OnlyCompatibility()
49+
assert.True(t, c.IsIPv6Compatible())
50+
assert.False(t, c.IsIPv4Compatible())
51+
}
52+
4753
func TestIsIPv6Only(t *testing.T) {
4854
tests := []struct {
4955
name string

agent/dockerclient/dockerapi/docker_client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ func (dg *dockerGoClient) Version(ctx context.Context, timeout time.Duration) (s
13001300
}
13011301

13021302
version = info.Version
1303+
seelog.Debugf("Determined the Docker server version: %s", version)
13031304
dg.setDaemonVersion(version)
13041305
return version, nil
13051306
}

agent/engine/docker_task_engine.go

Lines changed: 207 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ import (
5656
"github.com/aws/amazon-ecs-agent/ecs-agent/logger/field"
5757
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/retry"
5858
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/ttime"
59+
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
5960
"github.com/aws/aws-sdk-go/aws"
6061
ep "github.com/aws/aws-sdk-go/aws/endpoints"
62+
"github.com/aws/smithy-go/ptr"
6163
"github.com/docker/docker/api/types"
6264
dockercontainer "github.com/docker/docker/api/types/container"
6365
"github.com/docker/docker/api/types/registry"
@@ -89,6 +91,8 @@ const (
8991
logDriverTypeFirelens = "awsfirelens"
9092
logDriverTypeFluentd = "fluentd"
9193
logDriverTypeAwslogs = "awslogs"
94+
awsLogsEndpointKey = "awslogs-endpoint"
95+
awsLogsRegionKey = "awslogs-region"
9296
logDriverTag = "tag"
9397
logDriverMode = "mode"
9498
logDriverBufferSize = "max-buffer-size"
@@ -97,6 +101,7 @@ const (
97101
logDriverFluentdAddress = "fluentd-address"
98102
dataLogDriverPath = "/data/firelens/"
99103
logDriverAsyncConnect = "fluentd-async-connect"
104+
logDriverAsync = "fluentd-async"
100105
logDriverSubSecondPrecision = "fluentd-sub-second-precision"
101106
logDriverBufferLimit = "fluentd-buffer-limit"
102107
dataLogDriverSocketPath = "/socket/fluent.sock"
@@ -1874,7 +1879,35 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
18741879
// Update the environment variables FLUENT_HOST and FLUENT_PORT depending on the supported network modes - bridge
18751880
// and awsvpc. For reference - https://docs.docker.com/config/containers/logging/fluentd/.
18761881
if hostConfig.LogConfig.Type == logDriverTypeFirelens {
1877-
hostConfig.LogConfig = getFirelensLogConfig(task, container, hostConfig, engine.cfg)
1882+
// We need the Docker server version in order to generate the appropriate firelens log config
1883+
dockerServerVersion, err := engine.Version()
1884+
if err != nil {
1885+
logger.Error("Failed to determine Docker server version for Firelens log config generation", logger.Fields{
1886+
field.TaskID: task.GetID(),
1887+
field.Container: container.Name,
1888+
field.Error: err,
1889+
})
1890+
return dockerapi.DockerContainerMetadata{
1891+
Error: dockerapi.CannotCreateContainerError{FromError: errors.Wrapf(versionErr,
1892+
"failed to create container - container uses awsfirelens log driver and we failed to "+
1893+
"determine the Docker server version for Firelens log config generation")},
1894+
}
1895+
}
1896+
logConfig, err := getFirelensLogConfig(task, container, hostConfig, engine.cfg, dockerServerVersion)
1897+
if err != nil {
1898+
logger.Error("Failed to generate the Firelens log config", logger.Fields{
1899+
field.TaskID: task.GetID(),
1900+
field.Container: container.Name,
1901+
field.Error: err,
1902+
})
1903+
return dockerapi.DockerContainerMetadata{
1904+
Error: dockerapi.CannotCreateContainerError{FromError: errors.Wrapf(err,
1905+
"failed to create container - container uses awsfirelens log driver and we failed to "+
1906+
"generate the Firelens log config")},
1907+
}
1908+
}
1909+
hostConfig.LogConfig = logConfig
1910+
18781911
if task.IsNetworkModeAWSVPC() {
18791912
container.MergeEnvironmentVariables(map[string]string{
18801913
fluentNetworkHost: FluentAWSVPCHostValue,
@@ -1916,29 +1949,33 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
19161949

19171950
// This is a short term solution only for specific regions
19181951
if hostConfig.LogConfig.Type == logDriverTypeAwslogs {
1919-
region := engine.cfg.AWSRegion
1920-
if _, ok := unresolvedIsolatedRegions[region]; ok {
1921-
endpoint := ""
1922-
dnsSuffix := ""
1923-
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
1924-
if !ok {
1925-
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
1926-
"region": region,
1927-
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
1928-
})
1929-
dnsSuffix = ep.AwsPartition().DNSSuffix()
1930-
} else {
1931-
resolvedEndpoint, err := partition.EndpointFor("logs", region)
1932-
if err == nil {
1933-
endpoint = resolvedEndpoint.URL
1952+
if engine.cfg.InstanceIPCompatibility.IsIPv6Only() {
1953+
engine.setAWSLogsDualStackEndpoint(task, container, hostConfig)
1954+
} else {
1955+
region := engine.cfg.AWSRegion
1956+
if _, ok := unresolvedIsolatedRegions[region]; ok {
1957+
endpoint := ""
1958+
dnsSuffix := ""
1959+
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
1960+
if !ok {
1961+
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
1962+
"region": region,
1963+
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
1964+
})
1965+
dnsSuffix = ep.AwsPartition().DNSSuffix()
19341966
} else {
1935-
dnsSuffix = partition.DNSSuffix()
1967+
resolvedEndpoint, err := partition.EndpointFor("logs", region)
1968+
if err == nil {
1969+
endpoint = resolvedEndpoint.URL
1970+
} else {
1971+
dnsSuffix = partition.DNSSuffix()
1972+
}
19361973
}
1974+
if endpoint == "" {
1975+
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
1976+
}
1977+
hostConfig.LogConfig.Config[awsLogsEndpointKey] = endpoint
19371978
}
1938-
if endpoint == "" {
1939-
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
1940-
}
1941-
hostConfig.LogConfig.Config["awslogs-endpoint"] = endpoint
19421979
}
19431980
}
19441981

@@ -2108,29 +2145,64 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
21082145
return metadata
21092146
}
21102147

2111-
func getFirelensLogConfig(task *apitask.Task, container *apicontainer.Container,
2112-
hostConfig *dockercontainer.HostConfig, cfg *config.Config) dockercontainer.LogConfig {
2148+
// getFirelensLogConfig generates the fluentd log driver's log config.
2149+
// Every container that wants to use Firelens for logging, gets one instance of the fluentd log driver associated to it.
2150+
func getFirelensLogConfig(task *apitask.Task,
2151+
container *apicontainer.Container,
2152+
hostConfig *dockercontainer.HostConfig,
2153+
cfg *config.Config, dockerServerVersion string) (dockercontainer.LogConfig, error) {
2154+
var firelensConfig dockercontainer.LogConfig
2155+
// Set the log driver type
2156+
firelensConfig.Type = logDriverTypeFluentd
2157+
// Start from the existing container host config
2158+
hostConfigLogConfig := hostConfig.LogConfig
2159+
// Initialize a config to store the different log driver options
2160+
firelensConfig.Config = make(map[string]string)
2161+
// Generate a tag based on the task ID
21132162
fields := strings.Split(task.Arn, "/")
21142163
taskID := fields[len(fields)-1]
21152164
tag := fmt.Sprintf(fluentTagDockerFormat, container.Name, taskID)
2165+
firelensConfig.Config[logDriverTag] = tag
2166+
// Construct the fluent socket address
21162167
fluentd := socketPathPrefix + filepath.Join(cfg.DataDirOnHost, dataLogDriverPath, taskID, dataLogDriverSocketPath)
2117-
logConfig := hostConfig.LogConfig
2118-
bufferLimit, bufferLimitExists := logConfig.Config[apitask.FirelensLogDriverBufferLimitOption]
2119-
logConfig.Type = logDriverTypeFluentd
2120-
logConfig.Config = make(map[string]string)
2121-
logConfig.Config[logDriverTag] = tag
2122-
logConfig.Config[logDriverFluentdAddress] = fluentd
2123-
logConfig.Config[logDriverAsyncConnect] = strconv.FormatBool(cfg.FirelensAsyncEnabled.Enabled())
2124-
logConfig.Config[logDriverSubSecondPrecision] = strconv.FormatBool(true)
2168+
firelensConfig.Config[logDriverFluentdAddress] = fluentd
2169+
// Enable sub-second precision
2170+
firelensConfig.Config[logDriverSubSecondPrecision] = strconv.FormatBool(true)
2171+
// Set the log driver buffer limit if passed via the task payload
2172+
bufferLimit, bufferLimitExists := hostConfigLogConfig.Config[apitask.FirelensLogDriverBufferLimitOption]
21252173
if bufferLimitExists {
2126-
logConfig.Config[logDriverBufferLimit] = bufferLimit
2174+
firelensConfig.Config[logDriverBufferLimit] = bufferLimit
2175+
}
2176+
// Determine whether to use the "fluentd-async" option or the legacy "fluentd-async-connect" option.
2177+
// "fluentd-async-connect" option was deprecated in Docker v20.10.0 and removed in v28.0.0.
2178+
// It was replaced with the "fluentd-async" option starting Docker v20.10.0.
2179+
// Docker v20.10.0 release notes: https://docs.docker.com/engine/release-notes/20.10/#logging-2
2180+
// Docker v28.0.0 release notes: https://docs.docker.com/engine/release-notes/28/#removed
2181+
// This change is not versioned and applies to all Docker client API versions.
2182+
// Hence, in order to preserve backwards-compatibility with Docker server versions older than v20.10.0,
2183+
// we need to continue using the older fluentd-async-connect option.
2184+
isAsyncCompatible, err := utils.Version(dockerServerVersion).Matches(">=20.10.0")
2185+
if err != nil {
2186+
logger.Error("Unable to determine whether the Docker server version is at least 20.10.0", logger.Fields{
2187+
field.TaskID: task.GetID(),
2188+
field.Container: container.Name,
2189+
field.Error: err,
2190+
})
2191+
return dockercontainer.LogConfig{}, errors.Wrapf(err,
2192+
"unable to determine whether the Docker server version is at least 20.10.0")
2193+
}
2194+
if isAsyncCompatible {
2195+
firelensConfig.Config[logDriverAsync] = strconv.FormatBool(cfg.FirelensAsyncEnabled.Enabled())
2196+
} else {
2197+
firelensConfig.Config[logDriverAsyncConnect] = strconv.FormatBool(cfg.FirelensAsyncEnabled.Enabled())
21272198
}
2199+
21282200
logger.Debug("Applying firelens log config for container", logger.Fields{
21292201
field.TaskID: task.GetID(),
21302202
field.Container: container.Name,
2131-
"config": logConfig,
2203+
"config": firelensConfig,
21322204
})
2133-
return logConfig
2205+
return firelensConfig, nil
21342206
}
21352207

21362208
func (engine *DockerTaskEngine) startContainer(task *apitask.Task, container *apicontainer.Container) dockerapi.DockerContainerMetadata {
@@ -2882,3 +2954,104 @@ func (engine *DockerTaskEngine) getDockerID(task *apitask.Task, container *apico
28822954
}
28832955
return dockerContainer.DockerID, nil
28842956
}
2957+
2958+
// Sets CloudWatch Logs dual stack endpoint as "awslogs-endpoint" option in the logging config.
2959+
// This is needed because awslogs driver that we consume from Docker does not support
2960+
// an option to enable dual stack endpoints, so customers have no way to enable dual stack endpoints
2961+
// that are needed in an IPv6-only environment.
2962+
func (engine *DockerTaskEngine) setAWSLogsDualStackEndpoint(
2963+
task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig,
2964+
) {
2965+
// Helper function to populate common logger.Fields
2966+
withAdditionalLoggerFields := func(additionalFields logger.Fields) logger.Fields {
2967+
fields := logger.Fields{field.TaskARN: task.Arn, field.ContainerName: container.Name}
2968+
for k, v := range additionalFields {
2969+
fields[k] = v
2970+
}
2971+
return fields
2972+
}
2973+
2974+
// Do nothing if endpoint is already set
2975+
if hostConfig.LogConfig.Config[awsLogsEndpointKey] != "" {
2976+
logger.Info(
2977+
fmt.Sprintf(
2978+
"%s is already set in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
2979+
awsLogsEndpointKey),
2980+
withAdditionalLoggerFields(logger.Fields{}),
2981+
)
2982+
return
2983+
}
2984+
2985+
// Region is required to resolve endpoint
2986+
region := hostConfig.LogConfig.Config[awsLogsRegionKey]
2987+
if region == "" {
2988+
logger.Warn(
2989+
fmt.Sprintf(
2990+
"%s not found in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
2991+
awsLogsRegionKey),
2992+
withAdditionalLoggerFields(logger.Fields{}),
2993+
)
2994+
return
2995+
}
2996+
2997+
// Docker versions older than 18.09.0 do not support awslogs-endpoint
2998+
// option. So, skip endpoint resolution for those Docker versions.
2999+
dockerVersion, err := engine.Version()
3000+
if err != nil {
3001+
logger.Error("Failed to get Docker engine version. Skip resolving dual stack CloudWatch Logs endpoint.",
3002+
withAdditionalLoggerFields(logger.Fields{field.Error: err}))
3003+
return
3004+
}
3005+
const thresholdVersion = "18.09.0"
3006+
dockerVersionIsCompatible, err := utils.Version(dockerVersion).Matches(">=" + thresholdVersion)
3007+
if err != nil {
3008+
logger.Error("Failed to determine if docker version is high enough",
3009+
withAdditionalLoggerFields(logger.Fields{
3010+
field.Error: err,
3011+
field.DockerVersion: dockerVersion,
3012+
"thresholdVersion": thresholdVersion,
3013+
}))
3014+
return
3015+
}
3016+
if !dockerVersionIsCompatible {
3017+
logger.Warn(
3018+
fmt.Sprintf(
3019+
"Docker version does not support %s option. Skip resolving dual stack CloudWatch Logs endpoint.",
3020+
awsLogsEndpointKey),
3021+
withAdditionalLoggerFields(logger.Fields{
3022+
field.DockerVersion: dockerVersion,
3023+
"thresholdVersion": thresholdVersion,
3024+
}),
3025+
)
3026+
return
3027+
}
3028+
3029+
// Resolve the endpoint
3030+
endpoint, err := getAWSLogsDualStackEndpoint(region)
3031+
if err != nil {
3032+
logger.Error(
3033+
"Failed to get CloudWatch Logs dual stack endpoint. Skipping setting it.",
3034+
withAdditionalLoggerFields(logger.Fields{field.Region: region, field.Error: err}))
3035+
return
3036+
}
3037+
3038+
logger.Info("Resolved CloudWatch Logs dual stack endpoint",
3039+
withAdditionalLoggerFields(logger.Fields{
3040+
field.Endpoint: endpoint,
3041+
field.Region: region,
3042+
}))
3043+
hostConfig.LogConfig.Config[awsLogsEndpointKey] = endpoint
3044+
}
3045+
3046+
// Returns CloudWatch Logs dual stack endpoint for the given region.
3047+
func getAWSLogsDualStackEndpoint(region string) (string, error) {
3048+
endpoint, err := cloudwatchlogs.NewDefaultEndpointResolverV2().ResolveEndpoint(context.TODO(),
3049+
cloudwatchlogs.EndpointParameters{
3050+
UseDualStack: ptr.Bool(true),
3051+
Region: ptr.String(region),
3052+
})
3053+
if err != nil {
3054+
return "", fmt.Errorf("failed to resolve dual stack CloudWatch Logs endpoint for region '%s': %w", region, err)
3055+
}
3056+
return endpoint.URI.String(), nil
3057+
}

0 commit comments

Comments
 (0)