Skip to content

Commit a97def5

Browse files
committed
Suport container health check in Compose 3. Addresses aws#472.
1 parent fbcac16 commit a97def5

File tree

7 files changed

+139
-3
lines changed

7 files changed

+139
-3
lines changed

ecs-cli/modules/cli/compose/adapter/containerconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type ContainerConfig struct {
1818
Entrypoint []string
1919
Environment []*ecs.KeyValuePair
2020
ExtraHosts []*ecs.HostEntry
21+
HealthCheck *ecs.HealthCheck
2122
Hostname string
2223
Image string
2324
Links []string

ecs-cli/modules/cli/compose/adapter/convert.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"sort"
2222
"strconv"
2323
"strings"
24+
"time"
2425

2526
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils"
2627
"github.com/aws/aws-sdk-go/aws"
@@ -126,6 +127,28 @@ func ConvertToExtraHosts(cfgExtraHosts []string) ([]*ecs.HostEntry, error) {
126127
return extraHosts, nil
127128
}
128129

130+
// ConvertToHealthCheck converts a compose healthcheck to ECS healthcheck
131+
func ConvertToHealthCheck(healthCheckConfig *types.HealthCheckConfig) *ecs.HealthCheck {
132+
ecsHealthcheck := &ecs.HealthCheck{
133+
Command: aws.StringSlice(healthCheckConfig.Test),
134+
}
135+
// optional fields with defaults provided by ECS
136+
if healthCheckConfig.Interval != nil {
137+
ecsHealthcheck.Interval = ConvertToTimeInSeconds(healthCheckConfig.Interval)
138+
}
139+
if healthCheckConfig.Retries != nil {
140+
ecsHealthcheck.Retries = aws.Int64(int64(*healthCheckConfig.Retries))
141+
}
142+
if healthCheckConfig.Timeout != nil {
143+
ecsHealthcheck.Timeout = ConvertToTimeInSeconds(healthCheckConfig.Timeout)
144+
}
145+
if healthCheckConfig.StartPeriod != nil {
146+
ecsHealthcheck.StartPeriod = ConvertToTimeInSeconds(healthCheckConfig.StartPeriod)
147+
}
148+
149+
return ecsHealthcheck
150+
}
151+
129152
// ConvertToKeyValuePairs transforms the map of environment variables into list of ecs.KeyValuePair.
130153
// Environment variables with only a key are resolved by reading the variable from the shell where ecscli is executed from.
131154
// TODO: use this logic to generate RunTask overrides for ecscli compose commands (instead of always creating a new task def)
@@ -193,6 +216,12 @@ func ConvertToMemoryInMB(bytes int64) int64 {
193216
return memory
194217
}
195218

219+
// ConvertToTimeInSeconds converts a duration to an int64 number of seconds
220+
func ConvertToTimeInSeconds(d *time.Duration) *int64 {
221+
val := d.Nanoseconds() / 1E9
222+
return &val
223+
}
224+
196225
// ConvertToMountPoints transforms the yml volumes slice to ecs compatible MountPoints slice
197226
// It also uses the hostPath from volumes if present, else adds one to it
198227
func ConvertToMountPoints(cfgVolumes *yaml.Volumes, volumes *Volumes) ([]*ecs.MountPoint, error) {

ecs-cli/modules/cli/compose/adapter/convert_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import (
1919
"strconv"
2020
"strings"
2121
"testing"
22+
"time"
2223

2324
"github.com/aws/aws-sdk-go/aws"
2425
"github.com/aws/aws-sdk-go/service/ecs"
26+
"github.com/docker/cli/cli/compose/types"
2527
"github.com/docker/libcompose/config"
2628
"github.com/docker/libcompose/yaml"
2729
"github.com/stretchr/testify/assert"
@@ -663,3 +665,23 @@ func TestConvertCamelCaseToUnderScore(t *testing.T) {
663665
})
664666
}
665667
}
668+
669+
func TestConvertToHealthCheck(t *testing.T) {
670+
timeout := 10 * time.Second
671+
interval := time.Minute
672+
retries := uint64(3)
673+
startPeriod := 2 * time.Minute
674+
input := &types.HealthCheckConfig{
675+
Test: []string{"CMD", "echo 'echo is not a good health check test command'"},
676+
Timeout: &timeout,
677+
Interval: &interval,
678+
Retries: &retries,
679+
StartPeriod: &startPeriod,
680+
}
681+
output := ConvertToHealthCheck(input)
682+
assert.ElementsMatch(t, input.Test, aws.StringValueSlice(output.Command))
683+
assert.Equal(t, aws.Int64(10), output.Timeout)
684+
assert.Equal(t, aws.Int64(60), output.Interval)
685+
assert.Equal(t, aws.Int64(3), output.Retries)
686+
assert.Equal(t, aws.Int64(120), output.StartPeriod)
687+
}

ecs-cli/modules/cli/compose/logger/logger.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var supportedFieldsInV3 = map[string]bool{
7373
"EnvFile": true,
7474
"ExtraHosts": true,
7575
"Hostname": true,
76+
"HealthCheck": true,
7677
"Image": true,
7778
"Labels": true,
7879
"Links": true,

ecs-cli/modules/cli/compose/project/project_parseV3.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ func convertToContainerConfig(serviceConfig types.ServiceConfig, serviceVols *ad
9292
logger.LogUnsupportedV3ServiceConfigFields(serviceConfig)
9393
logWarningForDeployFields(serviceConfig.Deploy, serviceConfig.Name)
9494

95-
//TODO: Add Healthcheck, Devices to ContainerConfig
9695
c := &adapter.ContainerConfig{
9796
CapAdd: serviceConfig.CapAdd,
9897
CapDrop: serviceConfig.CapDrop,
@@ -115,6 +114,10 @@ func convertToContainerConfig(serviceConfig types.ServiceConfig, serviceVols *ad
115114
}
116115
c.Devices = devices
117116

117+
if serviceConfig.HealthCheck != nil && !serviceConfig.HealthCheck.Disable {
118+
c.HealthCheck = adapter.ConvertToHealthCheck(serviceConfig.HealthCheck)
119+
}
120+
118121
if serviceConfig.DNS != nil {
119122
c.DNSServers = serviceConfig.DNS
120123
}

ecs-cli/modules/cli/compose/project/project_parseV3_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ func TestParseV3WithOneFile(t *testing.T) {
4343
HostPath: aws.String("/dev/sda"),
4444
},
4545
}
46+
wordpressCon.HealthCheck = &ecs.HealthCheck{
47+
Command: aws.StringSlice([]string{"CMD-SHELL", "curl -f http://localhost || exit 1"}),
48+
Interval: aws.Int64(int64(90)),
49+
Timeout: aws.Int64(int64(10)),
50+
Retries: aws.Int64(int64(3)),
51+
}
4652
wordpressCon.DNSServers = []string{"2.2.2.2"}
4753
wordpressCon.DNSSearchDomains = []string{"wrd.search.com", "wrd.search2.com"}
4854
wordpressCon.Environment = []*ecs.KeyValuePair{
@@ -110,6 +116,13 @@ func TestParseV3WithOneFile(t *testing.T) {
110116
IpAddress: aws.String("10.0.0.0"),
111117
},
112118
}
119+
mysqlCon.HealthCheck = &ecs.HealthCheck{
120+
// when test command is specified as a string, compose wraps it in CMD-SHELL
121+
Command: aws.StringSlice([]string{"CMD-SHELL", "curl -f http://example.com || exit 1"}),
122+
Interval: aws.Int64(int64(105)),
123+
Timeout: aws.Int64(int64(15)),
124+
Retries: aws.Int64(int64(5)),
125+
}
113126

114127
// set up file
115128
composeFileString := `version: '3'
@@ -119,7 +132,7 @@ services:
119132
- ALL
120133
cap_drop:
121134
- NET_ADMIN
122-
command:
135+
command:
123136
- echo "hello world"
124137
image: wordpress
125138
entrypoint: /wordpress/entry
@@ -162,14 +175,24 @@ services:
162175
nice:
163176
soft: 300
164177
hard: 500
178+
healthcheck:
179+
test: ["CMD-SHELL", "curl -f http://localhost || exit 1"]
180+
interval: 1m30s
181+
timeout: 10s
182+
retries: 3
165183
mysql:
166184
image: mysql
167185
labels:
168186
- "com.example.mysql=mysqllabel"
169187
- "com.example.mysql2=anothermysql label"
170188
user: mysqluser
171189
extra_hosts:
172-
- "mysqlexhost:10.0.0.0"`
190+
- "mysqlexhost:10.0.0.0"
191+
healthcheck:
192+
test: curl -f http://example.com || exit 1
193+
interval: 1m45s
194+
timeout: 15s
195+
retries: 5`
173196

174197
tmpfile, err := ioutil.TempFile("", "test")
175198
assert.NoError(t, err, "Unexpected error in creating test file")
@@ -608,6 +631,53 @@ services:
608631
assert.ElementsMatch(t, expectedEnv, web.Environment, "Expected Environment to match")
609632
}
610633

634+
func TestParseV3HealthCheckDisabled(t *testing.T) {
635+
// set up expected ContainerConfig values
636+
wordpressCon := adapter.ContainerConfig{}
637+
wordpressCon.Name = "wordpress"
638+
wordpressCon.Command = []string{"echo \"hello world\""}
639+
wordpressCon.Image = "wordpress"
640+
641+
// set up file
642+
composeFileString := `version: '3'
643+
services:
644+
wordpress:
645+
command:
646+
- echo "hello world"
647+
image: wordpress
648+
healthcheck:
649+
test: ["CMD-SHELL", "curl -f http://localhost || exit 1"]
650+
interval: 1m30s
651+
timeout: 10s
652+
retries: 3
653+
disable: true`
654+
655+
tmpfile, err := ioutil.TempFile("", "test")
656+
assert.NoError(t, err, "Unexpected error in creating test file")
657+
658+
defer os.Remove(tmpfile.Name())
659+
660+
_, err = tmpfile.Write([]byte(composeFileString))
661+
assert.NoError(t, err, "Unexpected error writing file")
662+
663+
err = tmpfile.Close()
664+
assert.NoError(t, err, "Unexpected error closing file")
665+
666+
// add files to projects
667+
project := setupTestProject(t)
668+
project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name())
669+
670+
// assert # and content of container configs matches expected
671+
actualConfigs, err := project.parseV3()
672+
assert.NoError(t, err, "Unexpected error parsing file")
673+
674+
assert.Equal(t, 1, len(*actualConfigs))
675+
676+
wp, err := getContainerConfigByName(wordpressCon.Name, actualConfigs)
677+
assert.NoError(t, err, "Unexpected error retrieving wordpress config")
678+
verifyContainerConfig(t, wordpressCon, *wp)
679+
}
680+
611681
// TODO: add check for fields not used by V3, use to also check V1V2 ContainerConfigs?
612682
func verifyContainerConfig(t *testing.T, expected, actual adapter.ContainerConfig) {
613683
assert.ElementsMatch(t, expected.CapAdd, actual.CapAdd, "Expected CapAdd to match")
@@ -634,4 +704,13 @@ func verifyContainerConfig(t *testing.T, expected, actual adapter.ContainerConfi
634704
assert.ElementsMatch(t, expected.Ulimits, actual.Ulimits, "Expected Ulimits to match")
635705
assert.Equal(t, expected.User, actual.User, "Expected User to match")
636706
assert.Equal(t, expected.WorkingDirectory, actual.WorkingDirectory, "Expected WorkingDirectory to match")
707+
if expected.HealthCheck != nil && actual.HealthCheck != nil {
708+
assert.ElementsMatch(t, aws.StringValueSlice(expected.HealthCheck.Command), aws.StringValueSlice(actual.HealthCheck.Command), "Expected healthcheck command to match")
709+
assert.Equal(t, expected.HealthCheck.Interval, actual.HealthCheck.Interval, "Expected healthcheck interval to match")
710+
assert.Equal(t, expected.HealthCheck.Retries, actual.HealthCheck.Retries, "Expected healthcheck retries to match")
711+
assert.Equal(t, expected.HealthCheck.StartPeriod, actual.HealthCheck.StartPeriod, "Expected healthcheck start_period to match")
712+
assert.Equal(t, expected.HealthCheck.Timeout, actual.HealthCheck.Timeout, "Expected healthcheck timeout to match")
713+
} else {
714+
assert.Nil(t, actual.HealthCheck, "Expected healthcheck to be nil in output ContainerConfig")
715+
}
637716
}

ecs-cli/modules/utils/compose/convert_task_definition.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func convertToContainerDef(inputCfg *adapter.ContainerConfig, ecsContainerDef *C
116116
if inputCfg.Hostname != "" {
117117
outputContDef.SetHostname(inputCfg.Hostname)
118118
}
119+
outputContDef.SetHealthCheck(inputCfg.HealthCheck)
119120
outputContDef.SetImage(inputCfg.Image)
120121
outputContDef.SetLinks(aws.StringSlice(inputCfg.Links)) // TODO, read from external links
121122
outputContDef.SetLogConfiguration(inputCfg.LogConfiguration)

0 commit comments

Comments
 (0)