// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package config import ( "encoding/json" "fmt" "io" "os" "strconv" "strings" "time" "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/cihub/seelog" cniTypes "github.com/containernetworking/cni/pkg/types" "github.com/docker/go-connections/nat" ) const ( // envSkipDomainJoinCheck is an environment setting that can be used to skip // domain join check validation. This is useful for integration and // functional-tests but should not be set for any non-test use-case. envSkipDomainJoinCheck = "ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION" // envSkipDomainLessCheck is an environment setting that can be used to skip // domain less gMSA support check validation. This is useful for integration and // functional-tests but should not be set for any non-test use-case. envSkipDomainLessCheck = "ZZZ_SKIP_DOMAIN_LESS_CHECK_NOT_SUPPORTED_IN_PRODUCTION" // envGmsaEcsSupport is an environment setting that can be used to enable gMSA support on ECS envGmsaEcsSupport = "ECS_GMSA_SUPPORTED" // envCredentialsFetcherHostDir is an environment setting that is set in ecs-init identifying // location of the credentials-fetcher location on the machine envCredentialsFetcherHostDir = "CREDENTIALS_FETCHER_HOST_DIR" ) func parseCheckpoint(dataDir string) BooleanDefaultFalse { checkPoint := parseBooleanDefaultFalseConfig("ECS_CHECKPOINT") if dataDir != "" { // if we have a directory to checkpoint to, default it to be on if checkPoint.Value == NotSet { checkPoint.Value = ExplicitlyEnabled } } return checkPoint } func parseReservedPorts(env string) []uint16 { // Format: json array, e.g. [1,2,3] reservedPortEnv := os.Getenv(env) portDecoder := json.NewDecoder(strings.NewReader(reservedPortEnv)) var reservedPorts []uint16 err := portDecoder.Decode(&reservedPorts) // EOF means the string was blank as opposed to UnexepctedEof which means an // invalid parse // Blank is not a warning; we have sane defaults if err != io.EOF && err != nil { err := fmt.Errorf("Invalid format for \"%s\" environment variable; expected a JSON array like [1,2,3]. err %v", env, err) seelog.Warn(err) } return reservedPorts } func parseDockerStopTimeout() time.Duration { var dockerStopTimeout time.Duration parsedStopTimeout := parseEnvVariableDuration("ECS_CONTAINER_STOP_TIMEOUT") if parsedStopTimeout >= minimumDockerStopTimeout { dockerStopTimeout = parsedStopTimeout // if the ECS_CONTAINER_STOP_TIMEOUT is invalid or empty, then the parsedStopTimeout // will be 0, in this case we should return a 0, // because the DockerStopTimeout will merge with the DefaultDockerStopTimeout, // only when the DockerStopTimeout is empty } else if parsedStopTimeout != 0 { // if the configured ECS_CONTAINER_STOP_TIMEOUT is smaller than minimumDockerStopTimeout, // DockerStopTimeout will be set to minimumDockerStopTimeout // if the ECS_CONTAINER_STOP_TIMEOUT is 0, empty or an invalid value, then DockerStopTimeout // will be set to defaultDockerStopTimeout during the config merge operation dockerStopTimeout = minimumDockerStopTimeout seelog.Warnf("Discarded invalid value for docker stop timeout, parsed as: %v", parsedStopTimeout) } return dockerStopTimeout } func parseContainerStartTimeout() time.Duration { var containerStartTimeout time.Duration parsedStartTimeout := parseEnvVariableDuration("ECS_CONTAINER_START_TIMEOUT") if parsedStartTimeout >= minimumContainerStartTimeout { containerStartTimeout = parsedStartTimeout // do the parsedStartTimeout != 0 check for the same reason as in getDockerStopTimeout() } else if parsedStartTimeout != 0 { containerStartTimeout = minimumContainerStartTimeout seelog.Warnf("Discarded invalid value for container start timeout, parsed as: %v", parsedStartTimeout) } return containerStartTimeout } func parseContainerCreateTimeout() time.Duration { var containerCreateTimeout time.Duration parsedCreateTimeout := parseEnvVariableDuration("ECS_CONTAINER_CREATE_TIMEOUT") if parsedCreateTimeout >= minimumContainerCreateTimeout { containerCreateTimeout = parsedCreateTimeout // do the parsedCreateTimeout != 0 check for the same reason as in getDockerStopTimeout() } else if parsedCreateTimeout != 0 { containerCreateTimeout = minimumContainerCreateTimeout seelog.Warnf("Discarded invalid value for container create timeout, parsed as: %v", parsedCreateTimeout) } return containerCreateTimeout } func parseImagePullInactivityTimeout() time.Duration { var imagePullInactivityTimeout time.Duration parsedImagePullInactivityTimeout := parseEnvVariableDuration("ECS_IMAGE_PULL_INACTIVITY_TIMEOUT") if parsedImagePullInactivityTimeout >= minimumImagePullInactivityTimeout { imagePullInactivityTimeout = parsedImagePullInactivityTimeout // do the parsedStartTimeout != 0 check for the same reason as in getDockerStopTimeout() } else if parsedImagePullInactivityTimeout != 0 { imagePullInactivityTimeout = minimumImagePullInactivityTimeout seelog.Warnf("Discarded invalid value for image pull inactivity timeout, parsed as: %v", parsedImagePullInactivityTimeout) } return imagePullInactivityTimeout } func parseAvailableLoggingDrivers() []dockerclient.LoggingDriver { availableLoggingDriversEnv := os.Getenv("ECS_AVAILABLE_LOGGING_DRIVERS") loggingDriverDecoder := json.NewDecoder(strings.NewReader(availableLoggingDriversEnv)) var availableLoggingDrivers []dockerclient.LoggingDriver err := loggingDriverDecoder.Decode(&availableLoggingDrivers) // EOF means the string was blank as opposed to UnexpectedEof which means an // invalid parse // Blank is not a warning; we have sane defaults if err != io.EOF && err != nil { err := fmt.Errorf("Invalid format for \"ECS_AVAILABLE_LOGGING_DRIVERS\" environment variable; expected a JSON array like [\"json-file\",\"syslog\"]. err %v", err) seelog.Warn(err) } return availableLoggingDrivers } func parseVolumePluginCapabilities() []string { capsFromEnv := os.Getenv("ECS_VOLUME_PLUGIN_CAPABILITIES") if capsFromEnv == "" { return []string{} } capsDecoder := json.NewDecoder(strings.NewReader(capsFromEnv)) var caps []string err := capsDecoder.Decode(&caps) if err != nil { seelog.Warnf("Invalid format for \"ECS_VOLUME_PLUGIN_CAPABILITIES\", expected a json list of string. error: %v", err) } return caps } func parseNumImagesToDeletePerCycle() int { numImagesToDeletePerCycleEnvVal := os.Getenv("ECS_NUM_IMAGES_DELETE_PER_CYCLE") numImagesToDeletePerCycle, err := strconv.Atoi(numImagesToDeletePerCycleEnvVal) if numImagesToDeletePerCycleEnvVal != "" && err != nil { seelog.Warnf("Invalid format for \"ECS_NUM_IMAGES_DELETE_PER_CYCLE\", expected an integer. err %v", err) } return numImagesToDeletePerCycle } func parseNumNonECSContainersToDeletePerCycle() int { numNonEcsContainersToDeletePerCycleEnvVal := os.Getenv("NONECS_NUM_CONTAINERS_DELETE_PER_CYCLE") numNonEcsContainersToDeletePerCycle, err := strconv.Atoi(numNonEcsContainersToDeletePerCycleEnvVal) if numNonEcsContainersToDeletePerCycleEnvVal != "" && err != nil { seelog.Warnf("Invalid format for \"NONECS_NUM_CONTAINERS_DELETE_PER_CYCLE\", expected an integer. err %v", err) } return numNonEcsContainersToDeletePerCycle } func parseImagePullBehavior() ImagePullBehaviorType { ImagePullBehaviorString := os.Getenv("ECS_IMAGE_PULL_BEHAVIOR") switch ImagePullBehaviorString { case "always": return ImagePullAlwaysBehavior case "once": return ImagePullOnceBehavior case "prefer-cached": return ImagePullPreferCachedBehavior default: // Use the default image pull behavior when ECS_IMAGE_PULL_BEHAVIOR is // "default" or not valid return ImagePullDefaultBehavior } } func parseInstanceAttributes(errs []error) (map[string]string, []error) { var instanceAttributes map[string]string instanceAttributesEnv := os.Getenv("ECS_INSTANCE_ATTRIBUTES") err := json.Unmarshal([]byte(instanceAttributesEnv), &instanceAttributes) if instanceAttributesEnv != "" { if err != nil { wrappedErr := fmt.Errorf("Invalid format for ECS_INSTANCE_ATTRIBUTES. Expected a json hash: %v", err) seelog.Error(wrappedErr) errs = append(errs, wrappedErr) } } for attributeKey, attributeValue := range instanceAttributes { seelog.Debugf("Setting instance attribute %v: %v", attributeKey, attributeValue) } return instanceAttributes, errs } func parseAdditionalLocalRoutes(errs []error) ([]cniTypes.IPNet, []error) { var additionalLocalRoutes []cniTypes.IPNet additionalLocalRoutesEnv := os.Getenv("ECS_AWSVPC_ADDITIONAL_LOCAL_ROUTES") if additionalLocalRoutesEnv != "" { err := json.Unmarshal([]byte(additionalLocalRoutesEnv), &additionalLocalRoutes) if err != nil { seelog.Errorf("Invalid format for ECS_AWSVPC_ADDITIONAL_LOCAL_ROUTES, expected a json array of CIDRs: %v", err) errs = append(errs, err) } } return additionalLocalRoutes, errs } func parseBooleanDefaultFalseConfig(envVarName string) BooleanDefaultFalse { boolDefaultFalseCofig := BooleanDefaultFalse{Value: NotSet} configString := strings.TrimSpace(os.Getenv(envVarName)) if configString == "" { // if intentionally not set, do not add warning log return boolDefaultFalseCofig } res, err := strconv.ParseBool(configString) if err == nil { if res { boolDefaultFalseCofig.Value = ExplicitlyEnabled } else { boolDefaultFalseCofig.Value = ExplicitlyDisabled } } else { seelog.Warnf("Invalid format for \"%s\", expected a boolean. err %v", envVarName, err) } return boolDefaultFalseCofig } func parseBooleanDefaultTrueConfig(envVarName string) BooleanDefaultTrue { boolDefaultTrueCofig := BooleanDefaultTrue{Value: NotSet} configString := strings.TrimSpace(os.Getenv(envVarName)) if configString == "" { // if intentionally not set, do not add warning log return boolDefaultTrueCofig } res, err := strconv.ParseBool(configString) if err == nil { if res { boolDefaultTrueCofig.Value = ExplicitlyEnabled } else { boolDefaultTrueCofig.Value = ExplicitlyDisabled } } else { seelog.Warnf("Invalid format for \"%s\", expected a boolean. err %v", envVarName, err) } return boolDefaultTrueCofig } func parseTaskMetadataThrottles() (int, int) { var steadyStateRate, burstRate int rpsLimitEnvVal := os.Getenv("ECS_TASK_METADATA_RPS_LIMIT") if rpsLimitEnvVal == "" { seelog.Debug("Environment variable empty: ECS_TASK_METADATA_RPS_LIMIT") return 0, 0 } rpsLimitSplits := strings.Split(rpsLimitEnvVal, ",") if len(rpsLimitSplits) != 2 { seelog.Warn(`Invalid format for "ECS_TASK_METADATA_RPS_LIMIT", expected: "rateLimit,burst"`) return 0, 0 } steadyStateRate, err := strconv.Atoi(strings.TrimSpace(rpsLimitSplits[0])) if err != nil { seelog.Warnf(`Invalid format for "ECS_TASK_METADATA_RPS_LIMIT", expected integer for steady state rate: %v`, err) return 0, 0 } burstRate, err = strconv.Atoi(strings.TrimSpace(rpsLimitSplits[1])) if err != nil { seelog.Warnf(`Invalid format for "ECS_TASK_METADATA_RPS_LIMIT", expected integer for burst rate: %v`, err) return 0, 0 } return steadyStateRate, burstRate } func parseContainerInstanceTags(errs []error) (map[string]string, []error) { var containerInstanceTags map[string]string containerInstanceTagsConfigString := os.Getenv("ECS_CONTAINER_INSTANCE_TAGS") // If duplicate keys exist, the value of the key will be the value of latter key. err := json.Unmarshal([]byte(containerInstanceTagsConfigString), &containerInstanceTags) if containerInstanceTagsConfigString != "" { if err != nil { wrappedErr := fmt.Errorf("Invalid format for ECS_CONTAINER_INSTANCE_TAGS. Expected a json hash: %v", err) seelog.Error(wrappedErr) errs = append(errs, wrappedErr) } } for tagKey, tagValue := range containerInstanceTags { seelog.Debugf("Setting instance tag %v: %v", tagKey, tagValue) } return containerInstanceTags, errs } func parseContainerInstancePropagateTagsFrom() ContainerInstancePropagateTagsFromType { containerInstancePropagateTagsFromString := os.Getenv("ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM") switch containerInstancePropagateTagsFromString { case "ec2_instance": return ContainerInstancePropagateTagsFromEC2InstanceType default: // Use the default "none" type when ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM is // "none" or not valid. return ContainerInstancePropagateTagsFromNoneType } } func parseEnvVariableUint16(envVar string) uint16 { envVal := os.Getenv(envVar) var var16 uint16 if envVal != "" { var64, err := strconv.ParseUint(envVal, 10, 16) if err != nil { seelog.Warnf("Invalid format for \""+envVar+"\" environment variable; expected unsigned integer. err %v", err) } else { var16 = uint16(var64) } } return var16 } func parseEnvVariableDuration(envVar string) time.Duration { var duration time.Duration envVal := os.Getenv(envVar) if envVal == "" { seelog.Debugf("Environment variable empty: %v", envVar) } else { var err error duration, err = time.ParseDuration(envVal) if err != nil { seelog.Warnf("Could not parse duration value: %v for Environment Variable %v : %v", envVal, envVar, err) } } return duration } func parseImageCleanupExclusionList(envVar string) []string { imageEnv := os.Getenv(envVar) var imageCleanupExclusionList []string if imageEnv == "" { seelog.Debugf("Environment variable empty: %s", imageEnv) return nil } else { imageCleanupExclusionList = strings.Split(imageEnv, ",") } return imageCleanupExclusionList } func parseCgroupCPUPeriod() time.Duration { duration := parseEnvVariableDuration("ECS_CGROUP_CPU_PERIOD") if duration >= minimumCgroupCPUPeriod && duration <= maximumCgroupCPUPeriod { return duration } else if duration != 0 { seelog.Warnf("CPU Period duration value: %v for Environment Variable ECS_CGROUP_CPU_PERIOD is not within [%v, %v], using default value instead", duration, minimumCgroupCPUPeriod, maximumCgroupCPUPeriod) } return defaultCgroupCPUPeriod } var getDynamicHostPortRange = utils.GetDynamicHostPortRange func parseDynamicHostPortRange(dynamicHostPortRangeEnv string) string { dynamicHostPortRange := os.Getenv(dynamicHostPortRangeEnv) if dynamicHostPortRange != "" { _, _, err := nat.ParsePortRangeToInt(dynamicHostPortRange) if err != nil { seelog.Warnf("Invalid dynamicHostPortRange value from config: %s, err: %v", dynamicHostPortRange, err) return getDefaultDynamicHostPortRange() } } else { return getDefaultDynamicHostPortRange() } return dynamicHostPortRange } func getDefaultDynamicHostPortRange() string { startHostPortRange, endHostPortRange, err := getDynamicHostPortRange() if err != nil { seelog.Warnf("Unable to read the ephemeral host port range, "+ "falling back to the default range: %v-%v", utils.DefaultPortRangeStart, utils.DefaultPortRangeEnd) return fmt.Sprintf("%d-%d", utils.DefaultPortRangeStart, utils.DefaultPortRangeEnd) } return fmt.Sprintf("%d-%d", startHostPortRange, endHostPortRange) }