Skip to content

Commit 1283259

Browse files
committed
Added support for defining docker volume configuration in ECS Params
1 parent e264b7a commit 1283259

File tree

5 files changed

+233
-25
lines changed

5 files changed

+233
-25
lines changed

README.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ task_definition:
427427
cpu_shares: integer
428428
mem_limit: string
429429
mem_reservation: string
430+
docker_volumes:
431+
- name: string
432+
scope: string // Valid values: "shared" | "task"
433+
autoprovision: boolean // only valid if scope = "shared"
434+
driver: string
435+
driver_opts:
436+
string: string
437+
labels:
438+
string: string
430439
431440
run_params:
432441
network_configuration:
@@ -459,19 +468,7 @@ Fields listed under `task_definition` correspond to fields that will be included
459468
* In Docker compose version 2, the `cpu_shares`, `mem_limit`, and `mem_reservation` fields can be specified in either the compose or ECS params file. If they are specified in the ECS params file, the values will override values present in the compose file.
460469
* If you are using a private repository for pulling images, `repository_credentials` allows you to specify an AWS Secrets Manager secret ARN for the name of the secret containing your private repository credentials as a `credential_parameter`.
461470

462-
Example `ecs-params.yml` with service resources specified:
463-
```
464-
version: 1
465-
task_definition:
466-
services:
467-
wordpress:
468-
cpu_shares: 100
469-
mem_limit: 500m
470-
mysql:
471-
cpu_shares: 105
472-
mem_limit: 500m
473-
mem_reservation: 450m
474-
```
471+
* `docker_volumes` allows you to create docker volumes. The name key is required, and `scope`, `autoprovision`, `driver`, `driver_opts` and `labels` correspond with the fields under [dockerVolumeConfiguration](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-volumes.html) in an ECS Task Definition. Volumes defined with the `docker_volumes` key can be referenced in your compose file by name, even if they were not also specified in the compose file.
475472

476473
* `task_execution_role` should be the ARN of an IAM role. **NOTE**: This field is required to enable ECS Tasks to be configured with Cloudwatch Logs, or to pull images from ECR for your tasks.
477474

@@ -505,8 +502,20 @@ task_definition:
505502
ecs_network_mode: host
506503
task_role_arn: myCustomRole
507504
services:
508-
my_service:
505+
logging:
509506
essential: false
507+
wordpress:
508+
cpu_shares: 100
509+
mem_limit: 500m
510+
mysql:
511+
cpu_shares: 105
512+
mem_limit: 500m
513+
mem_reservation: 450m
514+
docker_volumes:
515+
- name: database_volume
516+
scope: shared
517+
autoprovision: true
518+
driver: local
510519
```
511520

512521
Example `ecs-params.yml` with network configuration with **EC2** launch type:

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,15 @@ func ConvertToTaskDefinition(taskDefinitionName string, volumes *adapter.Volumes
8080
containerDefinitions = append(containerDefinitions, containerDef)
8181
}
8282

83+
ecsVolumes, err := convertToECSVolumes(volumes, ecsParams)
84+
if err != nil {
85+
return nil, err
86+
}
87+
8388
taskDefinition := &ecs.TaskDefinition{
8489
Family: aws.String(taskDefinitionName),
8590
ContainerDefinitions: containerDefinitions,
86-
Volumes: convertToECSVolumes(volumes),
91+
Volumes: ecsVolumes,
8792
TaskRoleArn: aws.String(taskRoleArn),
8893
NetworkMode: aws.String(taskDefParams.networkMode),
8994
Cpu: aws.String(taskDefParams.cpu),
@@ -291,7 +296,7 @@ func showResourceOverrideMsg(serviceName string, val int64, override int64, opti
291296
}
292297

293298
// convertToECSVolumes transforms the map of hostPaths to the format of ecs.Volume
294-
func convertToECSVolumes(hostPaths *adapter.Volumes) []*ecs.Volume {
299+
func convertToECSVolumes(hostPaths *adapter.Volumes, ecsParams *ECSParams) ([]*ecs.Volume, error) {
295300
output := []*ecs.Volume{}
296301
// volumes with a host path
297302
for hostPath, volName := range hostPaths.VolumeWithHost {
@@ -302,14 +307,54 @@ func convertToECSVolumes(hostPaths *adapter.Volumes) []*ecs.Volume {
302307
}}
303308
output = append(output, ecsVolume)
304309
}
305-
// volumes with an empty host path
306-
for _, volName := range hostPaths.VolumeEmptyHost {
310+
311+
// volumes without host path (allowed to have Docker Volume Configuration)
312+
volumesWithoutHost, err := mergeVolumesWithoutHost(hostPaths.VolumeEmptyHost, ecsParams)
313+
if err != nil {
314+
return nil, err
315+
}
316+
output = append(output, volumesWithoutHost...)
317+
return output, nil
318+
}
319+
320+
func mergeVolumesWithoutHost(composeVolumes []string, ecsParams *ECSParams) ([]*ecs.Volume, error) {
321+
volumesWithoutHost := make(map[string]DockerVolume)
322+
output := []*ecs.Volume{}
323+
324+
for _, volName := range composeVolumes {
325+
volumesWithoutHost[volName] = DockerVolume{}
326+
}
327+
328+
if ecsParams != nil {
329+
for _, dockerVol := range ecsParams.TaskDefinition.DockerVolumes {
330+
if dockerVol.Name != "" {
331+
volumesWithoutHost[dockerVol.Name] = dockerVol
332+
} else {
333+
return nil, fmt.Errorf("Name is required when specifying a docker volume")
334+
}
335+
}
336+
}
337+
338+
for volName, dVol := range volumesWithoutHost {
307339
ecsVolume := &ecs.Volume{
308340
Name: aws.String(volName),
309341
}
342+
if dVol.Name != "" {
343+
ecsVolume.DockerVolumeConfiguration = &ecs.DockerVolumeConfiguration{
344+
Autoprovision: dVol.Autoprovision,
345+
Driver: aws.String(dVol.Driver),
346+
Scope: aws.String(dVol.Scope),
347+
}
348+
if dVol.DriverOptions != nil {
349+
ecsVolume.DockerVolumeConfiguration.DriverOpts = aws.StringMap(dVol.DriverOptions)
350+
}
351+
if dVol.Labels != nil {
352+
ecsVolume.DockerVolumeConfiguration.Labels = aws.StringMap(dVol.Labels)
353+
}
354+
}
310355
output = append(output, ecsVolume)
311356
}
312-
return output
357+
return output, nil
313358
}
314359

315360
func hasEssential(ecsParamsContainerDefs ContainerDefs, count int) bool {

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

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
containerPath2 = "/tmp/cache2"
3434
hostPath = "./cache"
3535
namedVolume = "named_volume"
36+
namedVolume2 = "named_volume2"
37+
namedVolume3 = "named_volume3"
3638
)
3739

3840
var defaultNetwork = &yaml.Network{
@@ -1182,7 +1184,7 @@ func TestConvertToTaskDefinitionWithVolumes(t *testing.T) {
11821184
{
11831185
ContainerPath: aws.String("/tmp/cache"),
11841186
ReadOnly: aws.Bool(false),
1185-
SourceVolume: aws.String("volume-3"),
1187+
SourceVolume: aws.String("volume-0"),
11861188
},
11871189
{
11881190
ContainerPath: aws.String("/tmp/cache"),
@@ -1214,6 +1216,89 @@ func TestConvertToTaskDefinitionWithVolumes(t *testing.T) {
12141216
assert.ElementsMatch(t, expectedVolumes, actualVolumes, "Expected volumes to match")
12151217
}
12161218

1219+
func TestConvertToTaskDefinitionWithVolumesWithHostOnly(t *testing.T) {
1220+
volumeConfigs := &adapter.Volumes{
1221+
VolumeWithHost: map[string]string{
1222+
hostPath: containerPath,
1223+
},
1224+
}
1225+
1226+
mountPoints := []*ecs.MountPoint{
1227+
{
1228+
ContainerPath: aws.String("/tmp/cache"),
1229+
ReadOnly: aws.Bool(false),
1230+
SourceVolume: aws.String("volume-0"),
1231+
},
1232+
}
1233+
containerConfig := adapter.ContainerConfig{
1234+
MountPoints: mountPoints,
1235+
}
1236+
1237+
containerConfigs := []adapter.ContainerConfig{containerConfig}
1238+
1239+
host := &ecs.HostVolumeProperties{SourcePath: aws.String(hostPath)}
1240+
expectedVolumes := []*ecs.Volume{
1241+
{
1242+
Host: host,
1243+
Name: aws.String(containerPath),
1244+
},
1245+
}
1246+
1247+
taskDefinition, err := ConvertToTaskDefinition(projectName, volumeConfigs, containerConfigs, "", "", nil)
1248+
assert.NoError(t, err, "Unexpected error converting Task Definition")
1249+
1250+
actualVolumes := taskDefinition.Volumes
1251+
assert.ElementsMatch(t, expectedVolumes, actualVolumes, "Expected volumes to match")
1252+
}
1253+
1254+
func TestConvertToTaskDefinitionWithECSParamsVolumeWithoutNameError(t *testing.T) {
1255+
volumeConfigs := &adapter.Volumes{
1256+
VolumeEmptyHost: []string{namedVolume, namedVolume2},
1257+
}
1258+
1259+
mountPoints := []*ecs.MountPoint{
1260+
{
1261+
ContainerPath: aws.String("/var/log"),
1262+
ReadOnly: aws.Bool(false),
1263+
SourceVolume: aws.String("named_volume"),
1264+
},
1265+
{
1266+
ContainerPath: aws.String("/tmp/cache"),
1267+
ReadOnly: aws.Bool(false),
1268+
SourceVolume: aws.String("named_volume2"),
1269+
},
1270+
}
1271+
containerConfig := adapter.ContainerConfig{
1272+
MountPoints: mountPoints,
1273+
}
1274+
1275+
containerConfigs := []adapter.ContainerConfig{containerConfig}
1276+
labels := map[string]string{
1277+
"testing.thisdoesntactuallyreallyadvancetheplot": "true",
1278+
}
1279+
options := map[string]string{
1280+
"Clyde": "says Goodbye Stranger, decides to Take The Long Way Home, and enjoys some Breakfast in America",
1281+
"He": "is a big fan of 70s music",
1282+
}
1283+
1284+
ecsParams := &ECSParams{
1285+
TaskDefinition: EcsTaskDef{
1286+
DockerVolumes: []DockerVolume{
1287+
DockerVolume{
1288+
Autoprovision: aws.Bool(true),
1289+
Scope: "shared",
1290+
Driver: "local",
1291+
DriverOptions: options,
1292+
Labels: labels,
1293+
},
1294+
},
1295+
},
1296+
}
1297+
1298+
_, err := ConvertToTaskDefinition(projectName, volumeConfigs, containerConfigs, "", "", ecsParams)
1299+
assert.Error(t, err, "Expected error converting Task Definition with ECS Params volume without name")
1300+
}
1301+
12171302
func TestIsZeroForEmptyConfig(t *testing.T) {
12181303
containerConfig := &adapter.ContainerConfig{}
12191304

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ type ECSParams struct {
4343

4444
// EcsTaskDef corresponds to fields in an ECS TaskDefinition
4545
type EcsTaskDef struct {
46-
NetworkMode string `yaml:"ecs_network_mode"`
47-
TaskRoleArn string `yaml:"task_role_arn"`
48-
ContainerDefinitions ContainerDefs `yaml:"services"`
49-
ExecutionRole string `yaml:"task_execution_role"`
50-
TaskSize TaskSize `yaml:"task_size"` // Needed to run FARGATE tasks
46+
NetworkMode string `yaml:"ecs_network_mode"`
47+
TaskRoleArn string `yaml:"task_role_arn"`
48+
ContainerDefinitions ContainerDefs `yaml:"services"`
49+
ExecutionRole string `yaml:"task_execution_role"`
50+
TaskSize TaskSize `yaml:"task_size"` // Needed to run FARGATE tasks
51+
DockerVolumes []DockerVolume `yaml:"docker_volumes"`
5152
}
5253

5354
// ContainerDefs is a map of ContainerDefs within a task definition
@@ -64,6 +65,15 @@ type ContainerDef struct {
6465
HealthCheck *HealthCheck `yaml:"healthcheck"`
6566
}
6667

68+
type DockerVolume struct {
69+
Name string `yaml:"name"`
70+
Scope string `yaml:"scope"`
71+
Autoprovision *bool `yaml:"autoprovision"`
72+
Driver string `yaml:"driver"`
73+
DriverOptions map[string]string `yaml:"driver_opts"`
74+
Labels map[string]string `yaml:"labels"`
75+
}
76+
6777
// HealthCheck holds all possible fields for HealthCheck, including fields
6878
// supported by docker compose vs ECS
6979
type HealthCheck struct {

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,65 @@ func TestConvertToECSPlacementStrategy(t *testing.T) {
550550
assert.ElementsMatch(t, expectedStrategy, ecsPlacementStrategy, "Expected placement strategy to match")
551551
}
552552
}
553+
func TestReadECSParams_WithDockerVolumes(t *testing.T) {
554+
ecsParamsString := `version: 1
555+
task_definition:
556+
docker_volumes:
557+
- name: my_volume
558+
scope: shared
559+
autoprovision: true
560+
driver: doggyromcom
561+
driver_opts:
562+
pudding: is-engaged-to-marry-Tum-Tum
563+
clyde: professes-his-love-at-the-ceremony
564+
it: does-not-go-well
565+
this: is-not-a-movie
566+
labels:
567+
pudding: mad
568+
clyde: sad
569+
life: sucks`
570+
571+
expectedVolumes := []DockerVolume{
572+
DockerVolume{
573+
Name: "my_volume",
574+
Scope: "shared",
575+
Autoprovision: aws.Bool(true),
576+
Driver: "doggyromcom",
577+
DriverOptions: map[string]string{
578+
"pudding": "is-engaged-to-marry-Tum-Tum",
579+
"clyde": "professes-his-love-at-the-ceremony",
580+
"it": "does-not-go-well",
581+
"this": "is-not-a-movie",
582+
},
583+
Labels: map[string]string{
584+
"pudding": "mad",
585+
"clyde": "sad",
586+
"life": "sucks",
587+
},
588+
},
589+
}
590+
591+
content := []byte(ecsParamsString)
592+
593+
tmpfile, err := ioutil.TempFile("", "ecs-params")
594+
assert.NoError(t, err, "Could not create ecs-params tempfile")
595+
596+
ecsParamsFileName := tmpfile.Name()
597+
defer os.Remove(ecsParamsFileName)
598+
599+
_, err = tmpfile.Write(content)
600+
assert.NoError(t, err, "Could not write data to ecs-params tempfile")
601+
602+
err = tmpfile.Close()
603+
assert.NoError(t, err, "Could not close tempfile")
604+
605+
ecsParams, err := ReadECSParams(ecsParamsFileName)
606+
607+
if assert.NoError(t, err) {
608+
volumes := ecsParams.TaskDefinition.DockerVolumes
609+
assert.ElementsMatch(t, expectedVolumes, volumes, "Expected volumes to match")
610+
}
611+
}
553612

554613
func TestReadECSParams_WithHealthCheck(t *testing.T) {
555614
ecsParamsString := `version: 1

0 commit comments

Comments
 (0)