Skip to content

[part 1] Create function to parse Linux /etc/os-release files to set detailed OS ENV Var, pass to httpclient, update README  #4705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 18, 2025
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ ecs-agent-*.tar
*.log
*.DS_Store
.run/
pkg/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ additional details on each available environment variable.
| `ECS_TASK_PIDS_LIMIT` | `100` | Specifies the per-task pids limit cgroup setting for each task launched on the container instance. This setting maps to the pids.max cgroup setting at the ECS task level. See https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#pid. If unset, pids will be unlimited. Min value is 1 and max value is 4194304 (4*1024*1024) | `unset` | Not Supported on Windows |
| `ECS_EBSTA_SUPPORTED` | `true` | Whether to use the container instance with EBS Task Attach support. This variable is set properly by ecs-init. Its value indicates if correct environment to support EBS volumes by instance has been set up or not. ECS only schedules EBSTA tasks if this feature is supported by the platform type. Check [EBS Volume considerations](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ebs-volumes.html#ebs-volume-considerations) for other EBS support details | `true` | Not Supported on Windows |
| `ECS_ENABLE_FIRELENS_ASYNC` | `true` | Whether the log driver connects to the Firelens container in the background. | `true` | `true` |
| `ECS_DETAILED_OS_FAMILY` | `debian_11` | Sets detailed OS information for Linux-based ECS instances by parsing /etc/os-release. This variable is set properly by ecs-init during system initialization. | `linux` | Not supported on Windows |

Additionally, the following environment variable(s) can be used to configure the behavior of the ecs-init service. When using ECS-Init, all env variables, including the ECS Agent variables above, are read from path `/etc/ecs/ecs.config`:
| Environment Variable Name | Example Value(s) | Description | Default value |
Expand Down
2 changes: 1 addition & 1 deletion agent/acs/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func NewUpdater(cfg *config.Config, state dockerstate.TaskEngineState, dataClien
taskEngine engine.TaskEngine) *updater {
return &updater{
config: cfg,
httpclient: httpclient.New(updateDownloadTimeout, false, agentversion.String(), config.OSType),
httpclient: httpclient.New(updateDownloadTimeout, false, agentversion.String(), config.OSType, ""),
state: state,
dataClient: dataClient,
taskEngine: taskEngine,
Expand Down
2 changes: 1 addition & 1 deletion agent/acs/updater/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func mocks(t *testing.T, cfg *config.Config) (*updater, *gomock.Controller, *moc

mockacs := mock_client.NewMockClientServer(ctrl)
mockhttp := mock_http.NewMockRoundTripper(ctrl)
httpClient := httpclient.New(updateDownloadTimeout, false, agentversion.String(), config.OSType)
httpClient := httpclient.New(updateDownloadTimeout, false, agentversion.String(), config.OSType, "")
httpClient.Transport.(httpclient.OverridableTransport).SetTransport(mockhttp)

u := NewUpdater(cfg, dockerstate.NewTaskEngineState(), data.NewNoopClient(),
Expand Down
2 changes: 1 addition & 1 deletion agent/asm/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (*asmClientCreator) NewASMClient(region string,
cfg, err := awsconfig.LoadDefaultConfig(
context.TODO(),
awsconfig.WithRegion(region),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType)),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType, "")),
awsconfig.WithCredentialsProvider(
awscreds.NewStaticCredentialsProvider(
creds.AccessKeyID,
Expand Down
4 changes: 4 additions & 0 deletions agent/config/config_accessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func (aca *agentConfigAccessor) OSType() string {
return OSType
}

func (aca *agentConfigAccessor) OSFamilyDetailed() string {
return GetDetailedOSFamily()
}

func (aca *agentConfigAccessor) ReservedMemory() uint16 {
return aca.cfg.ReservedMemory
}
Expand Down
19 changes: 19 additions & 0 deletions agent/config/os_family_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package config
import (
"testing"

"github.com/aws/amazon-ecs-agent/agent/statemanager/dependencies"
mock_dependencies "github.com/aws/amazon-ecs-agent/agent/statemanager/dependencies/mocks"

"github.com/golang/mock/gomock"
Expand All @@ -43,9 +44,15 @@ func getMockWindowsRegistryKey(ctrl *gomock.Controller) *mock_dependencies.MockR
return mockKey
}

func restoreGlobalState() {
winRegistry = dependencies.StdRegistry{}
windowsGetVersionFunc = windows.RtlGetVersion
}

func TestGetOSFamilyForWS2025Full(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(26100)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -60,6 +67,7 @@ func TestGetOSFamilyForWS2025Full(t *testing.T) {
func TestGetOSFamilyForWS2025Core(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(26100)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -74,6 +82,7 @@ func TestGetOSFamilyForWS2025Core(t *testing.T) {
func TestGetOSFamilyForWS2022Core(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(20348)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -88,6 +97,7 @@ func TestGetOSFamilyForWS2022Core(t *testing.T) {
func TestGetOSFamilyForWS2022Full(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(20348)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -102,6 +112,7 @@ func TestGetOSFamilyForWS2022Full(t *testing.T) {
func TestGetOSFamilyForWS2019Core(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(17763)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -116,6 +127,7 @@ func TestGetOSFamilyForWS2019Core(t *testing.T) {
func TestGetOSFamilyForWS2019Full(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(17763)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -130,6 +142,7 @@ func TestGetOSFamilyForWS2019Full(t *testing.T) {
func TestGetOSFamilyForWS2016Full(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(14393)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -144,6 +157,7 @@ func TestGetOSFamilyForWS2016Full(t *testing.T) {
func TestGetOSFamilyForWS2004Core(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(19041)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -158,6 +172,7 @@ func TestGetOSFamilyForWS2004Core(t *testing.T) {
func TestGetOSFamilyForWS20H2Core(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(19042)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -172,6 +187,7 @@ func TestGetOSFamilyForWS20H2Core(t *testing.T) {
func TestGetOSFamilyForInvalidBuildNumber(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(12345)
assert.Equal(t, unsupportedWindowsOSFamily, GetOSFamily())
Expand All @@ -180,6 +196,7 @@ func TestGetOSFamilyForInvalidBuildNumber(t *testing.T) {
func TestGetOSFamilyForKeyError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(19042)
mockWinRegistry := mock_dependencies.NewMockWindowsRegistry(ctrl)
Expand All @@ -196,6 +213,7 @@ func TestGetOSFamilyForKeyError(t *testing.T) {
func TestGetOSFamilyForInstallationTypeNotExistError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(19042)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand All @@ -210,6 +228,7 @@ func TestGetOSFamilyForInstallationTypeNotExistError(t *testing.T) {
func TestGetOSFamilyForInvalidInstallationType(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
defer restoreGlobalState()

windowsGetVersionFunc = fakeWindowsGetVersionFunc(19042)
mockKey := getMockWindowsRegistryKey(ctrl)
Expand Down
14 changes: 14 additions & 0 deletions agent/config/parse_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import (
"github.com/cihub/seelog"
)

const (
osFamilyEnvVar = "ECS_DETAILED_OS_FAMILY"
)

func parseGMSACapability() BooleanDefaultFalse {
envStatus := utils.ParseBool(os.Getenv(envGmsaEcsSupport), false)
if envStatus {
Expand Down Expand Up @@ -112,6 +116,16 @@ func GetOSFamily() string {
return strings.ToUpper(OSType)
}

// GetDetailedOSFamily returns the operating system family for linux based ecs instances.
// it first checks the ECS_DETAILED_OS_FAMILY environment variable set by ecs-init, and falls back to "LINUX" if not set
func GetDetailedOSFamily() string {
detailedOSFamily, ok := os.LookupEnv(osFamilyEnvVar)
if !ok {
return strings.ToUpper(OSType)
}
return detailedOSFamily
}

func parseTaskPidsLimit() int {
var taskPidsLimit int
pidsLimitEnvVal := os.Getenv("ECS_TASK_PIDS_LIMIT")
Expand Down
30 changes: 30 additions & 0 deletions agent/config/parse_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,33 @@ func TestParseTaskPidsLimit(t *testing.T) {
func TestParseTaskPidsLimit_Unset(t *testing.T) {
assert.Equal(t, 0, parseTaskPidsLimit())
}

func TestGetDetailedOSFamilyWithValidValue(t *testing.T) {
t.Setenv(osFamilyEnvVar, "debian_11")

result := GetDetailedOSFamily()
assert.Equal(t, "debian_11", result)
}

func TestGetDetailedOSFamilyWithEmptyValue(t *testing.T) {
t.Setenv(osFamilyEnvVar, "")

result := GetDetailedOSFamily()
assert.Equal(t, "", result)
}

func TestGetDetailedOSFamilyNotSet(t *testing.T) {
result := GetDetailedOSFamily()

assert.Equal(t, "LINUX", result)
}

// verifies that GetOSFamily() always returns "LINUX"
func TestGetOSFamilyAlwaysReturnsLinux(t *testing.T) {
osFamily := GetOSFamily()
assert.Equal(t, "LINUX", osFamily)

t.Setenv(osFamilyEnvVar, "debian_11")
osFamily = GetOSFamily()
assert.Equal(t, "LINUX", osFamily)
}
5 changes: 5 additions & 0 deletions agent/config/parse_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func GetOSFamily() string {
return strings.ToUpper(OSType)
}

// GetDetailedOSFamily returns the same as GetOSFamily() for unsupported platforms.
func GetDetailedOSFamily() string {
return GetOSFamily()
}

func parseTaskPidsLimit() int {
return 0
}
5 changes: 5 additions & 0 deletions agent/config/parse_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ func isDomainlessGmsaPluginInstalled() (bool, error) {
return false, nil
}

// GetDetailedOSFamily returns the same as GetOSFamily() on Windows for consistency with Linux.
func GetDetailedOSFamily() string {
return GetOSFamily()
}

func parseTaskPidsLimit() int {
pidsLimitEnvVal := os.Getenv("ECS_TASK_PIDS_LIMIT")
if pidsLimitEnvVal == "" {
Expand Down
14 changes: 14 additions & 0 deletions agent/config/parse_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"os"
"testing"

"github.com/aws/amazon-ecs-agent/agent/statemanager/dependencies"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/windows"
)

func TestParseGMSACapability(t *testing.T) {
Expand Down Expand Up @@ -142,3 +144,15 @@ func TestParseTaskPidsLimit(t *testing.T) {
func TestParseTaskPidsLimit_Unset(t *testing.T) {
assert.Equal(t, 0, parseTaskPidsLimit())
}

func TestGetDetailedOSFamilyWindows(t *testing.T) {
// GetDetailedOSFamily should return the same as GetOSFamily on Windows
defer func() {
winRegistry = dependencies.StdRegistry{}
windowsGetVersionFunc = windows.RtlGetVersion
}()

osFamily := GetOSFamily()
detailedOSFamily := GetDetailedOSFamily()
assert.Equal(t, osFamily, detailedOSFamily)
}
2 changes: 1 addition & 1 deletion agent/ecr/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const (
// NewECRFactory returns an ECRFactory capable of producing ECRSDK clients
func NewECRFactory(acceptInsecureCert bool, useDualStackEndpoint bool) ECRFactory {
return &ecrFactory{
httpClient: httpclient.New(roundtripTimeout, acceptInsecureCert, agentversion.String(), config.OSType),
httpClient: httpclient.New(roundtripTimeout, acceptInsecureCert, agentversion.String(), config.OSType, ""),
useDualStackEndpoint: useDualStackEndpoint,
}
}
Expand Down
2 changes: 1 addition & 1 deletion agent/fsx/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (*fsxClientCreator) NewFSxClient(region string,
context.TODO(),
awsconfig.WithRegion(region),
awsconfig.WithCredentialsProvider(staticCreds),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType)),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType, "")),
)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions agent/handlers/agentapi/taskprotection/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func (factory TaskProtectionClientFactory) NewTaskProtectionClient(
factory.AcceptInsecureCert,
version.String(),
config.OSType,
config.GetDetailedOSFamily(),
),
),
}
Expand Down
2 changes: 1 addition & 1 deletion agent/s3/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func isS3FIPSCompliantRegion(region string) bool {
// createAWSConfig returns a new AWS Config object that will be used to create new S3 clients
func createAWSConfig(region string, creds credentials.IAMRoleCredentials, useFIPSEndpoint, useDualStackEndpoint bool) (aws.Config, error) {
configOpts := []func(*awsconfig.LoadOptions) error{
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType)),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType, "")),
awsconfig.WithCredentialsProvider(
awscreds.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
),
Expand Down
2 changes: 1 addition & 1 deletion agent/ssm/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (*ssmClientCreator) NewSSMClient(region string,
ipCompatibility ipcompatibility.IPCompatibility) (ssmclient.SSMClient, error) {

opts := []func(*awsconfig.LoadOptions) error{
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType)),
awsconfig.WithHTTPClient(httpclient.New(roundtripTimeout, false, agentversion.String(), config.OSType, "")),
awsconfig.WithRegion(region),
awsconfig.WithCredentialsProvider(
awscreds.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ecs-agent/api/ecs/client/ecs_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func NewECSClient(
credentialsCache: credentialsCache,
configAccessor: configAccessor,
ec2metadata: ec2MetadataClient,
httpClient: httpclient.New(RoundtripTimeout, configAccessor.AcceptInsecureCert(), agentVer, configAccessor.OSType()),
httpClient: httpclient.New(RoundtripTimeout, configAccessor.AcceptInsecureCert(), agentVer, configAccessor.OSType(), configAccessor.OSFamilyDetailed()),
pollEndpointCache: async.NewTTLCache(&async.TTL{Duration: defaultPollEndpointCacheTTL}),
}

Expand Down
Loading
Loading