diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 8f94430d24..f462810fc1 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/efs" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/iam" @@ -47,6 +48,7 @@ type AWSClient struct { dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 ecsconn *ecs.ECS + efsconn *efs.EFS elbconn *elb.ELB autoscalingconn *autoscaling.AutoScaling s3conn *s3.S3 @@ -140,6 +142,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing ECS Connection") client.ecsconn = ecs.New(awsConfig) + log.Println("[INFO] Initializing EFS Connection") + client.efsconn = efs.New(awsConfig) + // aws-sdk-go uses v4 for signing requests, which requires all global // endpoints to use 'us-east-1'. // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 8596b844e5..f57cd47ea0 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -175,6 +175,7 @@ func Provider() terraform.ResourceProvider { "aws_ecs_cluster": resourceAwsEcsCluster(), "aws_ecs_service": resourceAwsEcsService(), "aws_ecs_task_definition": resourceAwsEcsTaskDefinition(), + "aws_efs_file_system": resourceAwsEfsFileSystem(), "aws_eip": resourceAwsEip(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_efs_file_system.go b/builtin/providers/aws/resource_aws_efs_file_system.go new file mode 100644 index 0000000000..4beae81e02 --- /dev/null +++ b/builtin/providers/aws/resource_aws_efs_file_system.go @@ -0,0 +1,165 @@ +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 resourceAwsEfsFileSystem() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEfsFileSystemCreate, + Read: resourceAwsEfsFileSystemRead, + Update: resourceAwsEfsFileSystemUpdate, + Delete: resourceAwsEfsFileSystemDelete, + + Schema: map[string]*schema.Schema{ + "reference_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsEfsFileSystemCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + referenceName := "" + if v, ok := d.GetOk("reference_name"); ok { + referenceName = v.(string) + "-" + } + token := referenceName + resource.UniqueId() + fs, err := conn.CreateFileSystem(&efs.CreateFileSystemInput{ + CreationToken: aws.String(token), + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating EFS file system: %s", *fs) + d.SetId(*fs.FileSystemId) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: "available", + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeFileSystems(&efs.DescribeFileSystemsInput{ + FileSystemId: aws.String(d.Id()), + }) + if err != nil { + return nil, "error", err + } + + if len(resp.FileSystems) < 1 { + return nil, "not-found", fmt.Errorf("EFS file system %q not found", d.Id()) + } + + fs := resp.FileSystems[0] + log.Printf("[DEBUG] current status of %q: %q", *fs.FileSystemId, *fs.LifeCycleState) + return fs, *fs.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 file system (%q) to create: %q", + d.Id(), err.Error()) + } + log.Printf("[DEBUG] EFS file system created: %q", *fs.FileSystemId) + + return resourceAwsEfsFileSystemUpdate(d, meta) +} + +func resourceAwsEfsFileSystemUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + err := setTagsEFS(conn, d) + if err != nil { + return err + } + + return resourceAwsEfsFileSystemRead(d, meta) +} + +func resourceAwsEfsFileSystemRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + resp, err := conn.DescribeFileSystems(&efs.DescribeFileSystemsInput{ + FileSystemId: aws.String(d.Id()), + }) + if err != nil { + return err + } + if len(resp.FileSystems) < 1 { + return fmt.Errorf("EFS file system %q not found", d.Id()) + } + + tagsResp, err := conn.DescribeTags(&efs.DescribeTagsInput{ + FileSystemId: aws.String(d.Id()), + }) + if err != nil { + return err + } + + d.Set("tags", tagsToMapEFS(tagsResp.Tags)) + + return nil +} + +func resourceAwsEfsFileSystemDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).efsconn + + log.Printf("[DEBUG] Deleting EFS file system %s", d.Id()) + _, err := conn.DeleteFileSystem(&efs.DeleteFileSystemInput{ + FileSystemId: aws.String(d.Id()), + }) + stateConf := &resource.StateChangeConf{ + Pending: []string{"available", "deleting"}, + Target: "", + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeFileSystems(&efs.DescribeFileSystemsInput{ + FileSystemId: aws.String(d.Id()), + }) + if err != nil { + efsErr, ok := err.(awserr.Error) + if ok && efsErr.Code() == "FileSystemNotFound" { + return nil, "", nil + } + return nil, "error", err + } + + if len(resp.FileSystems) < 1 { + return nil, "", nil + } + + fs := resp.FileSystems[0] + log.Printf("[DEBUG] current status of %q: %q", + *fs.FileSystemId, *fs.LifeCycleState) + return fs, *fs.LifeCycleState, nil + }, + Timeout: 10 * time.Minute, + Delay: 2 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + + log.Printf("[DEBUG] EFS file system %q deleted.", d.Id()) + + return nil +} diff --git a/builtin/providers/aws/resource_aws_efs_file_system_test.go b/builtin/providers/aws/resource_aws_efs_file_system_test.go new file mode 100644 index 0000000000..03e683476c --- /dev/null +++ b/builtin/providers/aws/resource_aws_efs_file_system_test.go @@ -0,0 +1,133 @@ +package aws + +import ( + "fmt" + "reflect" + "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 TestAccAWSEFSFileSystem(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEfsFileSystemDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEFSFileSystemConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckEfsFileSystem( + "aws_efs_file_system.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccAWSEFSFileSystemConfigWithTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckEfsFileSystem( + "aws_efs_file_system.foo-with-tags", + ), + testAccCheckEfsFileSystemTags( + "aws_efs_file_system.foo-with-tags", + map[string]string{ + "Name": "foo-efs", + "Another": "tag", + }, + ), + ), + }, + }, + }) +} + +func testAccCheckEfsFileSystemDestroy(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 testAccCheckEfsFileSystem(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 + _, err := conn.DescribeFileSystems(&efs.DescribeFileSystemsInput{ + FileSystemId: aws.String(fs.Primary.ID), + }) + + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckEfsFileSystemTags(resourceID string, expectedTags map[string]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 + resp, err := conn.DescribeTags(&efs.DescribeTagsInput{ + FileSystemId: aws.String(fs.Primary.ID), + }) + + if !reflect.DeepEqual(expectedTags, tagsToMapEFS(resp.Tags)) { + return fmt.Errorf("Tags mismatch.\nExpected: %#v\nGiven: %#v", + expectedTags, resp.Tags) + } + + if err != nil { + return err + } + + return nil + } +} + +const testAccAWSEFSFileSystemConfig = ` +resource "aws_efs_file_system" "foo" { + reference_name = "radeksimko" +} +` + +const testAccAWSEFSFileSystemConfigWithTags = ` +resource "aws_efs_file_system" "foo-with-tags" { + reference_name = "yada_yada" + tags { + Name = "foo-efs" + Another = "tag" + } +} +` diff --git a/builtin/providers/aws/tagsEFS.go b/builtin/providers/aws/tagsEFS.go new file mode 100644 index 0000000000..8303d68882 --- /dev/null +++ b/builtin/providers/aws/tagsEFS.go @@ -0,0 +1,94 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/efs" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsEFS(conn *efs.EFS, d *schema.ResourceData) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsEFS(tagsFromMapEFS(o), tagsFromMapEFS(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, 0, len(remove)) + for _, t := range remove { + k = append(k, t.Key) + } + _, err := conn.DeleteTags(&efs.DeleteTagsInput{ + FileSystemId: aws.String(d.Id()), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.CreateTags(&efs.CreateTagsInput{ + FileSystemId: aws.String(d.Id()), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsEFS(oldTags, newTags []*efs.Tag) ([]*efs.Tag, []*efs.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []*efs.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapEFS(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapEFS(m map[string]interface{}) []*efs.Tag { + var result []*efs.Tag + for k, v := range m { + result = append(result, &efs.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapEFS(ts []*efs.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/builtin/providers/aws/tagsEFS_test.go b/builtin/providers/aws/tagsEFS_test.go new file mode 100644 index 0000000000..ca2ae88436 --- /dev/null +++ b/builtin/providers/aws/tagsEFS_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/service/efs" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffEFSTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsEFS(tagsFromMapEFS(tc.Old), tagsFromMapEFS(tc.New)) + cm := tagsToMapEFS(c) + rm := tagsToMapEFS(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckEFSTags( + ts *[]*efs.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapEFS(*ts) + v, ok := m[key] + if value != "" && !ok { + return fmt.Errorf("Missing tag: %s", key) + } else if value == "" && ok { + return fmt.Errorf("Extra tag: %s", key) + } + if value == "" { + return nil + } + + if v != value { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +}