Skip to content
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

EFS Volume Configuration #2301

Merged
merged 7 commits into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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