diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f57cd47ea..350aeb75d 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -176,6 +176,7 @@ func Provider() terraform.ResourceProvider { "aws_ecs_service": resourceAwsEcsService(), "aws_ecs_task_definition": resourceAwsEcsTaskDefinition(), "aws_efs_file_system": resourceAwsEfsFileSystem(), + "aws_efs_mount_target": resourceAwsEfsMountTarget(), "aws_eip": resourceAwsEip(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_efs_mount_target.go b/builtin/providers/aws/resource_aws_efs_mount_target.go new file mode 100644 index 000000000..ca7656e63 --- /dev/null +++ b/builtin/providers/aws/resource_aws_efs_mount_target.go @@ -0,0 +1,223 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/efs" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsEfsMountTarget() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEfsMountTargetCreate, + Read: resourceAwsEfsMountTargetRead, + Update: resourceAwsEfsMountTargetUpdate, + Delete: resourceAwsEfsMountTargetDelete, + + Schema: map[string]*schema.Schema{ + "file_system_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + + "security_groups": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Computed: true, + Optional: true, + }, + + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network_interface_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsEfsMountTargetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + input := efs.CreateMountTargetInput{ + FileSystemId: aws.String(d.Get("file_system_id").(string)), + SubnetId: aws.String(d.Get("subnet_id").(string)), + } + + if v, ok := d.GetOk("ip_address"); ok { + input.IpAddress = aws.String(v.(string)) + } + if v, ok := d.GetOk("security_groups"); ok { + input.SecurityGroups = expandStringList(v.(*schema.Set).List()) + } + + log.Printf("[DEBUG] Creating EFS mount target: %#v", input) + + mt, err := conn.CreateMountTarget(&input) + if err != nil { + return err + } + + d.SetId(*mt.MountTargetId) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: "available", + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeMountTargets(&efs.DescribeMountTargetsInput{ + MountTargetId: aws.String(d.Id()), + }) + if err != nil { + return nil, "error", err + } + + if len(resp.MountTargets) < 1 { + return nil, "error", fmt.Errorf("EFS mount target %q not found", d.Id()) + } + + mt := resp.MountTargets[0] + + log.Printf("[DEBUG] Current status of %q: %q", *mt.MountTargetId, *mt.LifeCycleState) + return mt, *mt.LifeCycleState, nil + }, + Timeout: 10 * time.Minute, + Delay: 2 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for EFS mount target (%s) to create: %s", d.Id(), err) + } + + log.Printf("[DEBUG] EFS mount target created: %s", *mt.MountTargetId) + + return resourceAwsEfsMountTargetRead(d, meta) +} + +func resourceAwsEfsMountTargetUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + if d.HasChange("security_groups") { + input := efs.ModifyMountTargetSecurityGroupsInput{ + MountTargetId: aws.String(d.Id()), + SecurityGroups: expandStringList(d.Get("security_groups").(*schema.Set).List()), + } + _, err := conn.ModifyMountTargetSecurityGroups(&input) + if err != nil { + return err + } + } + + return resourceAwsEfsMountTargetRead(d, meta) +} + +func resourceAwsEfsMountTargetRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + resp, err := conn.DescribeMountTargets(&efs.DescribeMountTargetsInput{ + MountTargetId: aws.String(d.Id()), + }) + if err != nil { + return err + } + + if len(resp.MountTargets) < 1 { + return fmt.Errorf("EFS mount target %q not found", d.Id()) + } + + mt := resp.MountTargets[0] + + log.Printf("[DEBUG] Found EFS mount target: %#v", mt) + + d.SetId(*mt.MountTargetId) + d.Set("file_system_id", *mt.FileSystemId) + d.Set("ip_address", *mt.IpAddress) + d.Set("subnet_id", *mt.SubnetId) + d.Set("network_interface_id", *mt.NetworkInterfaceId) + + sgResp, err := conn.DescribeMountTargetSecurityGroups(&efs.DescribeMountTargetSecurityGroupsInput{ + MountTargetId: aws.String(d.Id()), + }) + if err != nil { + return err + } + + d.Set("security_groups", schema.NewSet(schema.HashString, flattenStringList(sgResp.SecurityGroups))) + + return nil +} + +func resourceAwsEfsMountTargetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + log.Printf("[DEBUG] Deleting EFS mount target %q", d.Id()) + _, err := conn.DeleteMountTarget(&efs.DeleteMountTargetInput{ + MountTargetId: aws.String(d.Id()), + }) + if err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"available", "deleting", "deleted"}, + Target: "", + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeMountTargets(&efs.DescribeMountTargetsInput{ + MountTargetId: aws.String(d.Id()), + }) + if err != nil { + awsErr, ok := err.(awserr.Error) + if !ok { + return nil, "error", err + } + + if awsErr.Code() == "MountTargetNotFound" { + return nil, "", nil + } + + return nil, "error", awsErr + } + + if len(resp.MountTargets) < 1 { + return nil, "", nil + } + + mt := resp.MountTargets[0] + + log.Printf("[DEBUG] Current status of %q: %q", *mt.MountTargetId, *mt.LifeCycleState) + return mt, *mt.LifeCycleState, nil + }, + Timeout: 10 * time.Minute, + Delay: 2 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for EFS mount target (%q) to delete: %q", + d.Id(), err.Error()) + } + + log.Printf("[DEBUG] EFS mount target %q deleted.", d.Id()) + + return nil +} diff --git a/builtin/providers/aws/resource_aws_efs_mount_target_test.go b/builtin/providers/aws/resource_aws_efs_mount_target_test.go new file mode 100644 index 000000000..e9d624e03 --- /dev/null +++ b/builtin/providers/aws/resource_aws_efs_mount_target_test.go @@ -0,0 +1,135 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/efs" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSEFSMountTarget(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEfsMountTargetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEFSMountTargetConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckEfsMountTarget( + "aws_efs_mount_target.alpha", + ), + ), + }, + resource.TestStep{ + Config: testAccAWSEFSMountTargetConfigModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckEfsMountTarget( + "aws_efs_mount_target.alpha", + ), + testAccCheckEfsMountTarget( + "aws_efs_mount_target.beta", + ), + ), + }, + }, + }) +} + +func testAccCheckEfsMountTargetDestroy(s *terraform.State) error { + if len(s.RootModule().Resources) > 0 { + return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources) + } + + return nil +} + +func testAccCheckEfsMountTarget(resourceID string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceID] + if !ok { + return fmt.Errorf("Not found: %s", resourceID) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + fs, ok := s.RootModule().Resources[resourceID] + if !ok { + return fmt.Errorf("Not found: %s", resourceID) + } + + conn := testAccProvider.Meta().(*AWSClient).efsconn + mt, err := conn.DescribeMountTargets(&efs.DescribeMountTargetsInput{ + MountTargetId: aws.String(fs.Primary.ID), + }) + if err != nil { + return err + } + + if *mt.MountTargets[0].MountTargetId != fs.Primary.ID { + return fmt.Errorf("Mount target ID mismatch: %q != %q", + *mt.MountTargets[0].MountTargetId, fs.Primary.ID) + } + + return nil + } +} + +const testAccAWSEFSMountTargetConfig = ` +resource "aws_efs_file_system" "foo" { + reference_name = "radeksimko" +} + +resource "aws_efs_mount_target" "alpha" { + file_system_id = "${aws_efs_file_system.foo.id}" + subnet_id = "${aws_subnet.alpha.id}" +} + +resource "aws_vpc" "foo" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "alpha" { + vpc_id = "${aws_vpc.foo.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +` + +const testAccAWSEFSMountTargetConfigModified = ` +resource "aws_efs_file_system" "foo" { + reference_name = "radeksimko" +} + +resource "aws_efs_mount_target" "alpha" { + file_system_id = "${aws_efs_file_system.foo.id}" + subnet_id = "${aws_subnet.alpha.id}" +} + +resource "aws_efs_mount_target" "beta" { + file_system_id = "${aws_efs_file_system.foo.id}" + subnet_id = "${aws_subnet.beta.id}" +} + +resource "aws_vpc" "foo" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "alpha" { + vpc_id = "${aws_vpc.foo.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} + +resource "aws_subnet" "beta" { + vpc_id = "${aws_vpc.foo.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +` diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 9b1c0ab79..d736e0ad5 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -368,7 +368,7 @@ func flattenElastiCacheParameters(list []*elasticache.Parameter) []map[string]in } // Takes the result of flatmap.Expand for an array of strings -// and returns a []string +// and returns a []*string func expandStringList(configured []interface{}) []*string { vs := make([]*string, 0, len(configured)) for _, v := range configured { @@ -377,6 +377,17 @@ func expandStringList(configured []interface{}) []*string { return vs } +// Takes list of pointers to strings. Expand to an array +// of raw strings and returns a []interface{} +// to keep compatibility w/ schema.NewSetschema.NewSet +func flattenStringList(list []*string) []interface{} { + vs := make([]interface{}, 0, len(list)) + for _, v := range list { + vs = append(vs, *v) + } + return vs +} + //Flattens an array of private ip addresses into a []string, where the elements returned are the IP strings e.g. "192.168.0.0" func flattenNetworkInterfacesPrivateIPAddresses(dtos []*ec2.NetworkInterfacePrivateIpAddress) []string { ips := make([]string, 0, len(dtos))