Skip to content

Commit

Permalink
EFS Volume Configuration (#2301)
Browse files Browse the repository at this point in the history
* Added EFSVolumeConfiguration (#2234)

* Added EFSVolumeConfiguration models

* Translate EFS volumes from ACS to Docker volume type

* fix gocyclo failure

* code review comments

* remove readonly config

* remove readonly options from code

* code review comments

* code review

* naming is hard

* Add efs capability (#2248)

* EFS functional testing (#2247)

* Add efs client to Gopkg.*

* Add efs client to vendor directory

* EFS functional test

* Reuse EFS filesystem and mount target(s)

* review fixups

* more code review fixups

* EFSVolumeConfiguration -> efsVolumeConfiguration (#2254)

* EFSVolumeConfiguration -> efsVolumeConfiguration

* Fix FileSystemId and write json marshalling unit tests

* unit test PostUnmarshalTask efsVolumeConfiguration behavior

* windows unit test fix

* Update ECS client and task defs (#2256)

* add memoryUnbounded to task volume marshal unit tests

* rebase mistake
  • Loading branch information
sparrc authored Dec 6, 2019
1 parent dde6b64 commit df03bef
Show file tree
Hide file tree
Showing 24 changed files with 4,046 additions and 15 deletions.
5 changes: 4 additions & 1 deletion agent/Gopkg.lock

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

13 changes: 11 additions & 2 deletions agent/acs/model/api/api-2.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@
}
},

"EFSVolumeConfiguration": {
"type":"structure",
"members":{
"fileSystemId":{"shape":"String"},
"rootDirectory":{"shape":"String"}
}
},
"ElasticNetworkInterface":{
"type":"structure",
"members":{
Expand Down Expand Up @@ -669,7 +676,8 @@
"name":{"shape":"String"},
"type":{"shape":"VolumeType"},
"host":{"shape":"HostVolumeProperties"},
"dockerVolumeConfiguration":{"shape":"DockerVolumeConfiguration"}
"dockerVolumeConfiguration":{"shape":"DockerVolumeConfiguration"},
"efsVolumeConfiguration":{"shape":"EFSVolumeConfiguration"}
}
},
"VolumeFrom":{
Expand All @@ -691,7 +699,8 @@
"type":"string",
"enum":[
"host",
"docker"
"docker",
"efs"
]
},
"TaskIdentifier": {
Expand Down
20 changes: 20 additions & 0 deletions agent/acs/model/ecsacs/api.go

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

85 changes: 80 additions & 5 deletions agent/api/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,22 @@ func TaskFromACS(acsTask *ecsacs.Task, envelope *ecsacs.PayloadMessage) (*Task,
return task, nil
}

func (task *Task) initializeVolumes(cfg *config.Config, dockerClient dockerapi.DockerClient, ctx context.Context) error {
err := task.initializeDockerLocalVolumes(dockerClient, ctx)
if err != nil {
return apierrors.NewResourceInitError(task.Arn, err)
}
err = task.initializeDockerVolumes(cfg.SharedVolumeMatchFullConfig, dockerClient, ctx)
if err != nil {
return apierrors.NewResourceInitError(task.Arn, err)
}
err = task.initializeEFSVolumes(cfg, dockerClient, ctx)
if err != nil {
return apierrors.NewResourceInitError(task.Arn, err)
}
return nil
}

// PostUnmarshalTask is run after a task has been unmarshalled, but before it has been
// run. It is possible it will be subsequently called after that and should be
// able to handle such an occurrence appropriately (e.g. behave idempotently).
Expand Down Expand Up @@ -322,11 +338,8 @@ func (task *Task) PostUnmarshalTask(cfg *config.Config,
task.initializeASMSecretResource(credentialsManager, resourceFields)
}

if err := task.initializeDockerLocalVolumes(dockerClient, ctx); err != nil {
return apierrors.NewResourceInitError(task.Arn, err)
}
if err := task.initializeDockerVolumes(cfg.SharedVolumeMatchFullConfig, dockerClient, ctx); err != nil {
return apierrors.NewResourceInitError(task.Arn, err)
if err := task.initializeVolumes(cfg, dockerClient, ctx); err != nil {
return err
}

if err := task.addGPUResource(cfg); err != nil {
Expand Down Expand Up @@ -496,6 +509,68 @@ func (task *Task) initializeDockerVolumes(sharedVolumeMatchFullConfig bool, dock
return nil
}

// initializeEFSVolumes inspects the volume definitions in the task definition.
// If it finds EFS volumes in the task definition, then it converts it to a docker
// volume definition.
func (task *Task) initializeEFSVolumes(cfg *config.Config, dockerClient dockerapi.DockerClient, ctx context.Context) error {
for i, vol := range task.Volumes {
// No need to do this for non-efs volume, eg: host bind/empty volume
if vol.Type != EFSVolumeType {
continue
}

efsvol, ok := vol.Volume.(*taskresourcevolume.EFSVolumeConfig)
if !ok {
return errors.New("task volume: volume configuration does not match the type 'efs'")
}

err := task.addEFSVolumes(ctx, cfg, dockerClient, &task.Volumes[i], efsvol)
if err != nil {
return err
}
}
return nil
}

// addEFSVolumes converts the EFS task definition into an internal docker 'local' volume
// mounted with NFS struct and updates container dependency
func (task *Task) addEFSVolumes(
ctx context.Context,
cfg *config.Config,
dockerClient dockerapi.DockerClient,
vol *TaskVolume,
efsvol *taskresourcevolume.EFSVolumeConfig,
) error {
// TODO CN and gov partition logic
// These are the NFS options recommended by EFS, see:
// https://docs.aws.amazon.com/efs/latest/ug/mounting-fs-mount-cmd-general.html
ostr := fmt.Sprintf("addr=%s.efs.%s.amazonaws.com,nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport", efsvol.FileSystemID, cfg.AWSRegion)
devstr := fmt.Sprintf(":%s", efsvol.RootDirectory)
volumeResource, err := taskresourcevolume.NewVolumeResource(
ctx,
vol.Name,
task.volumeName(vol.Name),
"task",
false,
"local",
map[string]string{
"type": "nfs",
"device": devstr,
"o": ostr,
},
map[string]string{},
dockerClient,
)
if err != nil {
return err
}

vol.Volume = &volumeResource.VolumeConfig
task.AddResource(resourcetype.DockerVolumeKey, volumeResource)
task.updateContainerVolumeDependency(vol.Name)
return nil
}

// addTaskScopedVolumes adds the task scoped volume into task resources and updates container dependency
func (task *Task) addTaskScopedVolumes(ctx context.Context, dockerClient dockerapi.DockerClient,
vol *TaskVolume) error {
Expand Down
74 changes: 74 additions & 0 deletions agent/api/task/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,80 @@ func TestPostUnmarshalTaskWithDockerVolumes(t *testing.T) {
assert.Equal(t, DockerVolumeType, taskVol.Type)
}

// Test that the PostUnmarshal function properly changes EfsVolumeConfiguration
// task definitions into a dockerVolumeConfiguration task resource.
func TestPostUnmarshalTaskWithEFSVolumes(t *testing.T) {
ctrl := gomock.NewController(t)
dockerClient := mock_dockerapi.NewMockDockerClient(ctrl)
dockerClient.EXPECT().InspectVolume(gomock.Any(), gomock.Any(), gomock.Any()).Return(dockerapi.SDKVolumeResponse{DockerVolume: &types.Volume{}})
taskFromACS := ecsacs.Task{
Arn: strptr("myArn"),
DesiredStatus: strptr("RUNNING"),
Family: strptr("myFamily"),
Version: strptr("1"),
Containers: []*ecsacs.Container{
{
Name: strptr("myName1"),
MountPoints: []*ecsacs.MountPoint{
{
ContainerPath: strptr("/some/path"),
SourceVolume: strptr("efsvolume"),
},
},
},
},
Volumes: []*ecsacs.Volume{
{
Name: strptr("efsvolume"),
Type: strptr("efs"),
EfsVolumeConfiguration: &ecsacs.EFSVolumeConfiguration{
FileSystemId: strptr("fs-12345"),
RootDirectory: strptr("/tmp"),
},
},
},
}
seqNum := int64(42)
task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum})
assert.Nil(t, err, "Should be able to handle acs task")
assert.Equal(t, 1, len(task.Containers)) // before PostUnmarshalTask
cfg := config.Config{}
cfg.AWSRegion = "us-west-2"
task.PostUnmarshalTask(&cfg, nil, nil, dockerClient, nil)
assert.Equal(t, 1, len(task.Containers), "Should match the number of containers as before PostUnmarshalTask")
assert.Equal(t, 1, len(task.Volumes), "Should have 1 volume")
taskVol := task.Volumes[0]
assert.Equal(t, "efsvolume", taskVol.Name)
assert.Equal(t, "efs", taskVol.Type)

resources := task.GetResources()
assert.Len(t, resources, 1)
vol, ok := resources[0].(*taskresourcevolume.VolumeResource)
require.True(t, ok)
dockerVolName := vol.VolumeConfig.DockerVolumeName
b, err := json.Marshal(resources[0])
require.NoError(t, err)
require.JSONEq(t, fmt.Sprintf(`{
"name": "efsvolume",
"dockerVolumeConfiguration": {
"scope": "task",
"autoprovision": false,
"mountPoint": "",
"driver": "local",
"driverOpts": {
"device": ":/tmp",
"o": "addr=fs-12345.efs.us-west-2.amazonaws.com,nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport",
"type": "nfs"
},
"labels": {},
"dockerVolumeName": "%s"
},
"createdAt": "0001-01-01T00:00:00Z",
"desiredStatus": "NONE",
"knownStatus": "NONE"
}`, dockerVolName), string(b))
}

func TestInitializeContainersV3MetadataEndpoint(t *testing.T) {
task := Task{
Containers: []*apicontainer.Container{
Expand Down
21 changes: 20 additions & 1 deletion agent/api/task/taskvolume.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
const (
HostVolumeType = "host"
DockerVolumeType = "docker"
EFSVolumeType = "efs"
)

// TaskVolume is a definition of all the volumes available for containers to
Expand Down Expand Up @@ -64,8 +65,10 @@ func (tv *TaskVolume) UnmarshalJSON(b []byte) error {
return tv.unmarshalHostVolume(intermediate["host"])
case DockerVolumeType:
return tv.unmarshalDockerVolume(intermediate["dockerVolumeConfiguration"])
case EFSVolumeType:
return tv.unmarshalEFSVolume(intermediate["efsVolumeConfiguration"])
default:
return errors.Errorf("invalid Volume: type must be docker or host, got %q", tv.Type)
return errors.Errorf("unrecognized volume type: %q", tv.Type)
}
}

Expand All @@ -85,6 +88,8 @@ func (tv *TaskVolume) MarshalJSON() ([]byte, error) {
result["dockerVolumeConfiguration"] = tv.Volume
case HostVolumeType:
result["host"] = tv.Volume
case EFSVolumeType:
result["efsVolumeConfiguration"] = tv.Volume
default:
return nil, errors.Errorf("unrecognized volume type: %q", tv.Type)
}
Expand All @@ -106,6 +111,20 @@ func (tv *TaskVolume) unmarshalDockerVolume(data json.RawMessage) error {
return nil
}

func (tv *TaskVolume) unmarshalEFSVolume(data json.RawMessage) error {
if data == nil {
return errors.New("invalid volume: empty volume configuration")
}
var efsVolumeConfig taskresourcevolume.EFSVolumeConfig
err := json.Unmarshal(data, &efsVolumeConfig)
if err != nil {
return err
}

tv.Volume = &efsVolumeConfig
return nil
}

func (tv *TaskVolume) unmarshalHostVolume(data json.RawMessage) error {
if data == nil {
return errors.New("invalid volume: empty volume configuration")
Expand Down
Loading

0 comments on commit df03bef

Please sign in to comment.