diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index badc3e20e..5eac34e8a 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/directoryservice" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" @@ -47,6 +48,7 @@ type Config struct { type AWSClient struct { cloudwatchconn *cloudwatch.CloudWatch cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs + dsconn *directoryservice.DirectoryService dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 ecsconn *ecs.ECS @@ -179,6 +181,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing OpsWorks Connection") client.opsworksconn = opsworks.New(usEast1AwsConfig) + + log.Println("[INFO] Initializing Directory Service connection") + client.dsconn = directoryservice.New(awsConfig) } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 42b1d6242..dc9a69960 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -170,6 +170,7 @@ func Provider() terraform.ResourceProvider { "aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_subnet_group": resourceAwsDbSubnetGroup(), + "aws_directory_service_directory": resourceAwsDirectoryServiceDirectory(), "aws_dynamodb_table": resourceAwsDynamoDbTable(), "aws_ebs_volume": resourceAwsEbsVolume(), "aws_ecs_cluster": resourceAwsEcsCluster(), diff --git a/builtin/providers/aws/resource_aws_directory_service_directory.go b/builtin/providers/aws/resource_aws_directory_service_directory.go new file mode 100644 index 000000000..1fdb9491e --- /dev/null +++ b/builtin/providers/aws/resource_aws_directory_service_directory.go @@ -0,0 +1,291 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/terraform/helper/resource" +) + +func resourceAwsDirectoryServiceDirectory() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDirectoryServiceDirectoryCreate, + Read: resourceAwsDirectoryServiceDirectoryRead, + Update: resourceAwsDirectoryServiceDirectoryUpdate, + Delete: resourceAwsDirectoryServiceDirectoryDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "size": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "alias": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "short_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "vpc_settings": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet_ids": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + "enable_sso": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "access_url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "dns_ip_addresses": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Computed: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsDirectoryServiceDirectoryCreate(d *schema.ResourceData, meta interface{}) error { + dsconn := meta.(*AWSClient).dsconn + + input := directoryservice.CreateDirectoryInput{ + Name: aws.String(d.Get("name").(string)), + Password: aws.String(d.Get("password").(string)), + Size: aws.String(d.Get("size").(string)), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + if v, ok := d.GetOk("short_name"); ok { + input.ShortName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("vpc_settings"); ok { + settings := v.([]interface{}) + + if len(settings) > 1 { + return fmt.Errorf("Only a single vpc_settings block is expected") + } else if len(settings) == 1 { + s := settings[0].(map[string]interface{}) + var subnetIds []*string + for _, id := range s["subnet_ids"].(*schema.Set).List() { + subnetIds = append(subnetIds, aws.String(id.(string))) + } + + vpcSettings := directoryservice.DirectoryVpcSettings{ + SubnetIds: subnetIds, + VpcId: aws.String(s["vpc_id"].(string)), + } + input.VpcSettings = &vpcSettings + } + } + + log.Printf("[DEBUG] Creating Directory Service: %s", input) + out, err := dsconn.CreateDirectory(&input) + if err != nil { + return err + } + log.Printf("[DEBUG] Directory Service created: %s", out) + d.SetId(*out.DirectoryId) + + // Wait for creation + log.Printf("[DEBUG] Waiting for DS (%q) to become available", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Requested", "Creating", "Created"}, + Target: "Active", + Refresh: func() (interface{}, string, error) { + resp, err := dsconn.DescribeDirectories(&directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(d.Id())}, + }) + if err != nil { + log.Printf("Error during creation of DS: %q", err.Error()) + return nil, "", err + } + + ds := resp.DirectoryDescriptions[0] + log.Printf("[DEBUG] Creation of DS %q is in following stage: %q.", + d.Id(), *ds.Stage) + return ds, *ds.Stage, nil + }, + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf( + "Error waiting for Directory Service (%s) to become available: %#v", + d.Id(), err) + } + + if v, ok := d.GetOk("alias"); ok { + d.SetPartial("alias") + + input := directoryservice.CreateAliasInput{ + DirectoryId: aws.String(d.Id()), + Alias: aws.String(v.(string)), + } + + log.Printf("[DEBUG] Assigning alias %q to DS directory %q", + v.(string), d.Id()) + out, err := dsconn.CreateAlias(&input) + if err != nil { + return err + } + log.Printf("[DEBUG] Alias %q assigned to DS directory %q", + *out.Alias, *out.DirectoryId) + } + + return resourceAwsDirectoryServiceDirectoryUpdate(d, meta) +} + +func resourceAwsDirectoryServiceDirectoryUpdate(d *schema.ResourceData, meta interface{}) error { + dsconn := meta.(*AWSClient).dsconn + + if d.HasChange("enable_sso") { + d.SetPartial("enable_sso") + var err error + + if v, ok := d.GetOk("enable_sso"); ok && v.(bool) { + log.Printf("[DEBUG] Enabling SSO for DS directory %q", d.Id()) + _, err = dsconn.EnableSso(&directoryservice.EnableSsoInput{ + DirectoryId: aws.String(d.Id()), + }) + } else { + log.Printf("[DEBUG] Disabling SSO for DS directory %q", d.Id()) + _, err = dsconn.DisableSso(&directoryservice.DisableSsoInput{ + DirectoryId: aws.String(d.Id()), + }) + } + + if err != nil { + return err + } + } + + return resourceAwsDirectoryServiceDirectoryRead(d, meta) +} + +func resourceAwsDirectoryServiceDirectoryRead(d *schema.ResourceData, meta interface{}) error { + dsconn := meta.(*AWSClient).dsconn + + input := directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(d.Id())}, + } + out, err := dsconn.DescribeDirectories(&input) + if err != nil { + return err + } + + dir := out.DirectoryDescriptions[0] + log.Printf("[DEBUG] Received DS directory: %s", *dir) + + d.Set("access_url", *dir.AccessUrl) + d.Set("alias", *dir.Alias) + if dir.Description != nil { + d.Set("description", *dir.Description) + } + d.Set("dns_ip_addresses", schema.NewSet(schema.HashString, flattenStringList(dir.DnsIpAddrs))) + d.Set("name", *dir.Name) + if dir.ShortName != nil { + d.Set("short_name", *dir.ShortName) + } + d.Set("size", *dir.Size) + d.Set("type", *dir.Type) + d.Set("vpc_settings", flattenDSVpcSettings(dir.VpcSettings)) + d.Set("enable_sso", *dir.SsoEnabled) + + return nil +} + +func resourceAwsDirectoryServiceDirectoryDelete(d *schema.ResourceData, meta interface{}) error { + dsconn := meta.(*AWSClient).dsconn + + input := directoryservice.DeleteDirectoryInput{ + DirectoryId: aws.String(d.Id()), + } + _, err := dsconn.DeleteDirectory(&input) + if err != nil { + return err + } + + // Wait for deletion + log.Printf("[DEBUG] Waiting for DS (%q) to be deleted", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Deleting"}, + Target: "", + Refresh: func() (interface{}, string, error) { + resp, err := dsconn.DescribeDirectories(&directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(d.Id())}, + }) + if err != nil { + return nil, "", err + } + + if len(resp.DirectoryDescriptions) == 0 { + return nil, "", nil + } + + ds := resp.DirectoryDescriptions[0] + log.Printf("[DEBUG] Deletion of DS %q is in following stage: %q.", + d.Id(), *ds.Stage) + return ds, *ds.Stage, nil + }, + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf( + "Error waiting for Directory Service (%s) to be deleted: %q", + d.Id(), err.Error()) + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_directory_service_directory_test.go b/builtin/providers/aws/resource_aws_directory_service_directory_test.go new file mode 100644 index 000000000..b10174bdb --- /dev/null +++ b/builtin/providers/aws/resource_aws_directory_service_directory_test.go @@ -0,0 +1,283 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSDirectoryServiceDirectory_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDirectoryServiceDirectoryDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDirectoryServiceDirectoryConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceDirectoryExists("aws_directory_service_directory.bar"), + ), + }, + }, + }) +} + +func TestAccAWSDirectoryServiceDirectory_withAliasAndSso(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDirectoryServiceDirectoryDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDirectoryServiceDirectoryConfig_withAlias, + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceDirectoryExists("aws_directory_service_directory.bar_a"), + testAccCheckServiceDirectoryAlias("aws_directory_service_directory.bar_a", + fmt.Sprintf("tf-d-%d", randomInteger)), + testAccCheckServiceDirectorySso("aws_directory_service_directory.bar_a", false), + ), + }, + resource.TestStep{ + Config: testAccDirectoryServiceDirectoryConfig_withSso, + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceDirectoryExists("aws_directory_service_directory.bar_a"), + testAccCheckServiceDirectoryAlias("aws_directory_service_directory.bar_a", + fmt.Sprintf("tf-d-%d", randomInteger)), + testAccCheckServiceDirectorySso("aws_directory_service_directory.bar_a", true), + ), + }, + resource.TestStep{ + Config: testAccDirectoryServiceDirectoryConfig_withSso_modified, + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceDirectoryExists("aws_directory_service_directory.bar_a"), + testAccCheckServiceDirectoryAlias("aws_directory_service_directory.bar_a", + fmt.Sprintf("tf-d-%d", randomInteger)), + testAccCheckServiceDirectorySso("aws_directory_service_directory.bar_a", false), + ), + }, + }, + }) +} + +func testAccCheckDirectoryServiceDirectoryDestroy(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 testAccCheckServiceDirectoryExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + dsconn := testAccProvider.Meta().(*AWSClient).dsconn + out, err := dsconn.DescribeDirectories(&directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if len(out.DirectoryDescriptions) < 1 { + return fmt.Errorf("No DS directory found") + } + + if *out.DirectoryDescriptions[0].DirectoryId != rs.Primary.ID { + return fmt.Errorf("DS directory ID mismatch - existing: %q, state: %q", + *out.DirectoryDescriptions[0].DirectoryId, rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckServiceDirectoryAlias(name, alias string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + dsconn := testAccProvider.Meta().(*AWSClient).dsconn + out, err := dsconn.DescribeDirectories(&directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if *out.DirectoryDescriptions[0].Alias != alias { + return fmt.Errorf("DS directory Alias mismatch - actual: %q, expected: %q", + *out.DirectoryDescriptions[0].Alias, alias) + } + + return nil + } +} + +func testAccCheckServiceDirectorySso(name string, ssoEnabled bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + dsconn := testAccProvider.Meta().(*AWSClient).dsconn + out, err := dsconn.DescribeDirectories(&directoryservice.DescribeDirectoriesInput{ + DirectoryIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if *out.DirectoryDescriptions[0].SsoEnabled != ssoEnabled { + return fmt.Errorf("DS directory SSO mismatch - actual: %t, expected: %t", + *out.DirectoryDescriptions[0].SsoEnabled, ssoEnabled) + } + + return nil + } +} + +const testAccDirectoryServiceDirectoryConfig = ` +resource "aws_directory_service_directory" "bar" { + name = "corp.notexample.com" + password = "SuperSecretPassw0rd" + size = "Small" + + vpc_settings { + vpc_id = "${aws_vpc.main.id}" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +resource "aws_subnet" "bar" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +` + +var randomInteger = genRandInt() +var testAccDirectoryServiceDirectoryConfig_withAlias = fmt.Sprintf(` +resource "aws_directory_service_directory" "bar_a" { + name = "corp.notexample.com" + password = "SuperSecretPassw0rd" + size = "Small" + alias = "tf-d-%d" + + vpc_settings { + vpc_id = "${aws_vpc.main.id}" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +resource "aws_subnet" "bar" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +`, randomInteger) + +var testAccDirectoryServiceDirectoryConfig_withSso = fmt.Sprintf(` +resource "aws_directory_service_directory" "bar_a" { + name = "corp.notexample.com" + password = "SuperSecretPassw0rd" + size = "Small" + alias = "tf-d-%d" + enable_sso = true + + vpc_settings { + vpc_id = "${aws_vpc.main.id}" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +resource "aws_subnet" "bar" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +`, randomInteger) + +var testAccDirectoryServiceDirectoryConfig_withSso_modified = fmt.Sprintf(` +resource "aws_directory_service_directory" "bar_a" { + name = "corp.notexample.com" + password = "SuperSecretPassw0rd" + size = "Small" + alias = "tf-d-%d" + enable_sso = false + + vpc_settings { + vpc_id = "${aws_vpc.main.id}" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +resource "aws_subnet" "bar" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +`, randomInteger) diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index b738027f8..5976a8ff0 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elasticache" @@ -590,3 +591,13 @@ func stringMapToPointers(m map[string]interface{}) map[string]*string { } return list } + +func flattenDSVpcSettings( + s *directoryservice.DirectoryVpcSettingsDescription) []map[string]interface{} { + settings := make(map[string]interface{}, 0) + + settings["subnet_ids"] = schema.NewSet(schema.HashString, flattenStringList(s.SubnetIds)) + settings["vpc_id"] = *s.VpcId + + return []map[string]interface{}{settings} +} diff --git a/website/source/docs/providers/aws/r/directory_service_directory.html.markdown b/website/source/docs/providers/aws/r/directory_service_directory.html.markdown new file mode 100644 index 000000000..04049ee55 --- /dev/null +++ b/website/source/docs/providers/aws/r/directory_service_directory.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "aws" +page_title: "AWS: aws_directory_service_directory" +sidebar_current: "docs-aws-resource-directory-service-directory" +description: |- + Provides a directory in AWS Directory Service. +--- + +# aws\_directory\_service\_directory + +Provides a directory in AWS Directory Service. + +## Example Usage + +``` +resource "aws_directory_service_directory" "bar" { + name = "corp.notexample.com" + password = "SuperSecretPassw0rd" + size = "Small" + + vpc_settings { + vpc_id = "${aws_vpc.main.id}" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2a" + cidr_block = "10.0.1.0/24" +} +resource "aws_subnet" "bar" { + vpc_id = "${aws_vpc.main.id}" + availability_zone = "us-west-2b" + cidr_block = "10.0.2.0/24" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The fully qualified name for the directory, such as `corp.example.com` +* `password` - (Required) The password for the directory administrator. +* `size` - (Required) The size of the directory (`Small` or `Large` are accepted values). +* `vpc_settings` - (Required) VPC related information about the directory. Fields documented below. +* `alias` - (Optional) The alias for the directory (must be unique amongst all aliases in AWS). Required for `enable_sso`. +* `description` - (Optional) A textual description for the directory. +* `short_name` - (Optional) The short name of the directory, such as `CORP`. +* `enable_sso` - (Optional) Whether to enable single-sign on for the directory. Requires `alias`. Defaults to `false`. + +**vpc\_settings** supports the following: + +* `subnet_ids` - (Required) The identifiers of the subnets for the directory servers (min. 2 subnets in 2 different AZs). +* `vpc_id` - (Required) The identifier of the VPC that the directory is in. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The directory identifier. +* `access_url` - The access URL for the directory, such as `http://alias.awsapps.com`. +* `dns_ip_addresses` - A list of IP addresses of the DNS servers for the directory. +* `type` - The directory type. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 0c33d54ab..743bc0b84 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -26,6 +26,16 @@ + > + Directory Service Resources + + > DynamoDB Resources