diff --git a/aws/resource_aws_dx_connection.go b/aws/resource_aws_dx_connection.go index 6074c395bab..fa3a955e3fe 100644 --- a/aws/resource_aws_dx_connection.go +++ b/aws/resource_aws_dx_connection.go @@ -2,9 +2,11 @@ package aws import ( "fmt" + "log" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/directconnect" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -14,25 +16,31 @@ func resourceAwsDxConnection() *schema.Resource { return &schema.Resource{ Create: resourceAwsDxConnectionCreate, Read: resourceAwsDxConnectionRead, + Update: resourceAwsDxConnectionUpdate, Delete: resourceAwsDxConnectionDelete, Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "bandwidth": &schema.Schema{ + "bandwidth": { Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: validateDxConnectionBandWidth, }, - "location": &schema.Schema{ + "location": { Type: schema.TypeString, Required: true, ForceNew: true, }, + "tags": tagsSchema(), }, } } @@ -40,53 +48,105 @@ func resourceAwsDxConnection() *schema.Resource { func resourceAwsDxConnectionCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - input := &directconnect.CreateConnectionInput{ + req := &directconnect.CreateConnectionInput{ Bandwidth: aws.String(d.Get("bandwidth").(string)), ConnectionName: aws.String(d.Get("name").(string)), Location: aws.String(d.Get("location").(string)), } - resp, err := conn.CreateConnection(input) + + log.Printf("[DEBUG] Creating Direct Connect connection: %#v", req) + resp, err := conn.CreateConnection(req) if err != nil { return err } - d.SetId(*resp.ConnectionId) - return resourceAwsDxConnectionRead(d, meta) + + d.SetId(aws.StringValue(resp.ConnectionId)) + return resourceAwsDxConnectionUpdate(d, meta) } func resourceAwsDxConnectionRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - connectionId := d.Id() - input := &directconnect.DescribeConnectionsInput{ - ConnectionId: aws.String(connectionId), - } - resp, err := conn.DescribeConnections(input) + resp, err := conn.DescribeConnections(&directconnect.DescribeConnectionsInput{ + ConnectionId: aws.String(d.Id()), + }) if err != nil { + if isNoSuchDxConnectionErr(err) { + log.Printf("[WARN] Direct Connect connection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } return err } + if len(resp.Connections) < 1 { + log.Printf("[WARN] Direct Connect connection (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if len(resp.Connections) != 1 { - return fmt.Errorf("[ERROR] Number of DX Connection (%s) isn't one, got %d", connectionId, len(resp.Connections)) + return fmt.Errorf("[ERROR] Number of Direct Connect connections (%s) isn't one, got %d", d.Id(), len(resp.Connections)) + } + connection := resp.Connections[0] + if d.Id() != aws.StringValue(connection.ConnectionId) { + return fmt.Errorf("[ERROR] Direct Connect connection (%s) not found", d.Id()) + } + if aws.StringValue(connection.ConnectionState) == directconnect.ConnectionStateDeleted { + log.Printf("[WARN] Direct Connect connection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil } - if d.Id() != *resp.Connections[0].ConnectionId { - return fmt.Errorf("[ERROR] DX Connection (%s) not found", connectionId) + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Service: "directconnect", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("dxcon/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("name", connection.ConnectionName) + d.Set("bandwidth", connection.Bandwidth) + d.Set("location", connection.Location) + + if err := getTagsDX(conn, d, arn); err != nil { + return err } + return nil } +func resourceAwsDxConnectionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Service: "directconnect", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("dxcon/%s", d.Id()), + }.String() + if err := setTagsDX(conn, d, arn); err != nil { + return err + } + + return resourceAwsDxConnectionRead(d, meta) +} + func resourceAwsDxConnectionDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - input := &directconnect.DeleteConnectionInput{ + log.Printf("[DEBUG] Deleting Direct Connect connection: %s", d.Id()) + _, err := conn.DeleteConnection(&directconnect.DeleteConnectionInput{ ConnectionId: aws.String(d.Id()), - } - _, err := conn.DeleteConnection(input) + }) if err != nil { + if isNoSuchDxConnectionErr(err) { + return nil + } return err } + deleteStateConf := &resource.StateChangeConf{ Pending: []string{directconnect.ConnectionStatePending, directconnect.ConnectionStateOrdering, directconnect.ConnectionStateAvailable, directconnect.ConnectionStateRequested, directconnect.ConnectionStateDeleting}, Target: []string{directconnect.ConnectionStateDeleted}, @@ -97,9 +157,9 @@ func resourceAwsDxConnectionDelete(d *schema.ResourceData, meta interface{}) err } _, err = deleteStateConf.WaitForState() if err != nil { - return fmt.Errorf("Error waiting for Dx Connection (%s) to be deleted: %s", d.Id(), err) + return fmt.Errorf("Error waiting for Direct Connect connection (%s) to be deleted: %s", d.Id(), err) } - d.SetId("") + return nil } @@ -112,6 +172,13 @@ func dxConnectionRefreshStateFunc(conn *directconnect.DirectConnect, connId stri if err != nil { return nil, "failed", err } + if len(resp.Connections) < 1 { + return resp, directconnect.ConnectionStateDeleted, nil + } return resp, *resp.Connections[0].ConnectionState, nil } } + +func isNoSuchDxConnectionErr(err error) bool { + return isAWSErr(err, "DirectConnectClientException", "Could not find Connection with ID") +} diff --git a/aws/resource_aws_dx_connection_test.go b/aws/resource_aws_dx_connection_test.go index f608470be1d..d442690daa3 100644 --- a/aws/resource_aws_dx_connection_test.go +++ b/aws/resource_aws_dx_connection_test.go @@ -12,15 +12,51 @@ import ( ) func TestAccAwsDxConnection_basic(t *testing.T) { + connectionName := fmt.Sprintf("tf-dx-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsDxConnectionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDxConnectionConfig(connectionName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxConnectionExists("aws_dx_connection.hoge"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "name", connectionName), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "bandwidth", "1Gbps"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "location", "EqSe2"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccAwsDxConnection_tags(t *testing.T) { + connectionName := fmt.Sprintf("tf-dx-%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAwsDxConnectionDestroy, Steps: []resource.TestStep{ { - Config: testAccDxConnectionConfig(acctest.RandString(5)), + Config: testAccDxConnectionConfig_tags(connectionName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsDxConnectionExists("aws_dx_connection.hoge"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "name", connectionName), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "tags.Usage", "original"), + ), + }, + { + Config: testAccDxConnectionConfig_tagsChanged(connectionName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxConnectionExists("aws_dx_connection.hoge"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "name", connectionName), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_dx_connection.hoge", "tags.Usage", "changed"), ), }, }, @@ -63,12 +99,41 @@ func testAccCheckAwsDxConnectionExists(name string) resource.TestCheckFunc { } } -func testAccDxConnectionConfig(rName string) string { +func testAccDxConnectionConfig(n string) string { + return fmt.Sprintf(` +resource "aws_dx_connection" "hoge" { + name = "%s" + bandwidth = "1Gbps" + location = "EqSe2" +} +`, n) +} + +func testAccDxConnectionConfig_tags(n string) string { + return fmt.Sprintf(` +resource "aws_dx_connection" "hoge" { + name = "%s" + bandwidth = "1Gbps" + location = "EqSe2" + + tags { + Environment = "production" + Usage = "original" + } +} +`, n) +} + +func testAccDxConnectionConfig_tagsChanged(n string) string { return fmt.Sprintf(` - resource "aws_dx_connection" "hoge" { - name = "tf-dx-%s" - bandwidth = "1Gbps" - location = "EqSe2" - } - `, rName) +resource "aws_dx_connection" "hoge" { + name = "%s" + bandwidth = "1Gbps" + location = "EqSe2" + + tags { + Usage = "changed" + } +} +`, n) } diff --git a/aws/resource_aws_dx_lag.go b/aws/resource_aws_dx_lag.go index 89dd99111f4..7aba3410405 100644 --- a/aws/resource_aws_dx_lag.go +++ b/aws/resource_aws_dx_lag.go @@ -2,9 +2,11 @@ package aws import ( "fmt" + "log" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/directconnect" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -18,31 +20,36 @@ func resourceAwsDxLag() *schema.Resource { Delete: resourceAwsDxLagDelete, Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { Type: schema.TypeString, Required: true, }, - "connections_bandwidth": &schema.Schema{ + "connections_bandwidth": { Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: validateDxConnectionBandWidth, }, - "location": &schema.Schema{ + "location": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "number_of_connections": &schema.Schema{ + "number_of_connections": { Type: schema.TypeInt, Required: true, ForceNew: true, }, - "force_destroy": &schema.Schema{ + "force_destroy": { Type: schema.TypeBool, Optional: true, Default: false, }, + "tags": tagsSchema(), }, } } @@ -50,89 +57,155 @@ func resourceAwsDxLag() *schema.Resource { func resourceAwsDxLagCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - input := &directconnect.CreateLagInput{ + req := &directconnect.CreateLagInput{ ConnectionsBandwidth: aws.String(d.Get("connections_bandwidth").(string)), LagName: aws.String(d.Get("name").(string)), Location: aws.String(d.Get("location").(string)), NumberOfConnections: aws.Int64(int64(d.Get("number_of_connections").(int))), } - resp, err := conn.CreateLag(input) + + log.Printf("[DEBUG] Creating Direct Connect LAG: %#v", req) + resp, err := conn.CreateLag(req) if err != nil { return err } - d.SetId(*resp.LagId) - return resourceAwsDxLagRead(d, meta) + + d.SetId(aws.StringValue(resp.LagId)) + return resourceAwsDxLagUpdate(d, meta) } func resourceAwsDxLagRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - lagId := d.Id() - input := &directconnect.DescribeLagsInput{ - LagId: aws.String(lagId), - } - resp, err := conn.DescribeLags(input) + resp, err := conn.DescribeLags(&directconnect.DescribeLagsInput{ + LagId: aws.String(d.Id()), + }) if err != nil { + if isNoSuchDxLagErr(err) { + log.Printf("[WARN] Direct Connect LAG (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } return err } + if len(resp.Lags) < 1 { + log.Printf("[WARN] Direct Connect LAG (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if len(resp.Lags) != 1 { - return fmt.Errorf("[ERROR] Number of DX Lag (%s) isn't one, got %d", lagId, len(resp.Lags)) + return fmt.Errorf("[ERROR] Number of Direct Connect LAGs (%s) isn't one, got %d", d.Id(), len(resp.Lags)) } - if d.Id() != *resp.Lags[0].LagId { - return fmt.Errorf("[ERROR] DX Lag (%s) not found", lagId) + lag := resp.Lags[0] + if d.Id() != aws.StringValue(lag.LagId) { + return fmt.Errorf("[ERROR] Direct Connect LAG (%s) not found", d.Id()) } + + if aws.StringValue(lag.LagState) == directconnect.LagStateDeleted { + log.Printf("[WARN] Direct Connect LAG (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Service: "directconnect", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("dxlag/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("name", lag.LagName) + d.Set("connections_bandwidth", lag.ConnectionsBandwidth) + d.Set("location", lag.Location) + d.Set("number_of_connections", lag.NumberOfConnections) + + if err := getTagsDX(conn, d, arn); err != nil { + return err + } + return nil } func resourceAwsDxLagUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn - input := &directconnect.UpdateLagInput{ - LagId: aws.String(d.Id()), - } + d.Partial(true) + if d.HasChange("name") { - input.LagName = aws.String(d.Get("name").(string)) + req := &directconnect.UpdateLagInput{ + LagId: aws.String(d.Id()), + LagName: aws.String(d.Get("name").(string)), + } + + log.Printf("[DEBUG] Updating Direct Connect LAG: %#v", req) + _, err := conn.UpdateLag(req) + if err != nil { + return err + } else { + d.SetPartial("name") + } } - _, err := conn.UpdateLag(input) - if err != nil { + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Service: "directconnect", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("dxlag/%s", d.Id()), + }.String() + if err := setTagsDX(conn, d, arn); err != nil { return err + } else { + d.SetPartial("tags") } - return nil + + d.Partial(false) + + return resourceAwsDxLagRead(d, meta) } func resourceAwsDxLagDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dxconn if d.Get("force_destroy").(bool) { - input := &directconnect.DescribeLagsInput{ + resp, err := conn.DescribeLags(&directconnect.DescribeLagsInput{ LagId: aws.String(d.Id()), - } - resp, err := conn.DescribeLags(input) + }) if err != nil { + if isNoSuchDxLagErr(err) { + return nil + } return err } + + if len(resp.Lags) < 1 { + return nil + } lag := resp.Lags[0] for _, v := range lag.Connections { - dcinput := &directconnect.DeleteConnectionInput{ + log.Printf("[DEBUG] Deleting Direct Connect connection: %s", aws.StringValue(v.ConnectionId)) + _, err := conn.DeleteConnection(&directconnect.DeleteConnectionInput{ ConnectionId: v.ConnectionId, - } - if _, err := conn.DeleteConnection(dcinput); err != nil { + }) + if err != nil && !isNoSuchDxConnectionErr(err) { return err } } } - input := &directconnect.DeleteLagInput{ + log.Printf("[DEBUG] Deleting Direct Connect LAG: %s", d.Id()) + _, err := conn.DeleteLag(&directconnect.DeleteLagInput{ LagId: aws.String(d.Id()), - } - _, err := conn.DeleteLag(input) + }) if err != nil { + if isNoSuchDxLagErr(err) { + return nil + } return err } + deleteStateConf := &resource.StateChangeConf{ Pending: []string{directconnect.LagStateAvailable, directconnect.LagStateRequested, directconnect.LagStatePending, directconnect.LagStateDeleting}, Target: []string{directconnect.LagStateDeleted}, @@ -143,9 +216,9 @@ func resourceAwsDxLagDelete(d *schema.ResourceData, meta interface{}) error { } _, err = deleteStateConf.WaitForState() if err != nil { - return fmt.Errorf("Error waiting for Dx Lag (%s) to be deleted: %s", d.Id(), err) + return fmt.Errorf("Error waiting for Direct Connect LAG (%s) to be deleted: %s", d.Id(), err) } - d.SetId("") + return nil } @@ -158,6 +231,13 @@ func dxLagRefreshStateFunc(conn *directconnect.DirectConnect, lagId string) reso if err != nil { return nil, "failed", err } + if len(resp.Lags) < 1 { + return resp, directconnect.LagStateDeleted, nil + } return resp, *resp.Lags[0].LagState, nil } } + +func isNoSuchDxLagErr(err error) bool { + return isAWSErr(err, "DirectConnectClientException", "Could not find Lag with ID") +} diff --git a/aws/resource_aws_dx_lag_test.go b/aws/resource_aws_dx_lag_test.go index 533d9ca4b01..54f4581647f 100644 --- a/aws/resource_aws_dx_lag_test.go +++ b/aws/resource_aws_dx_lag_test.go @@ -12,15 +12,72 @@ import ( ) func TestAccAwsDxLag_basic(t *testing.T) { + lagName1 := fmt.Sprintf("tf-dx-lag-%s", acctest.RandString(5)) + lagName2 := fmt.Sprintf("tf-dx-lag-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsDxLagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDxLagConfig(lagName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxLagExists("aws_dx_lag.hoge"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "name", lagName1), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "connections_bandwidth", "1Gbps"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "location", "EqSe2"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "number_of_connections", "2"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.%", "0"), + ), + }, + { + Config: testAccDxLagConfig(lagName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxLagExists("aws_dx_lag.hoge"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "name", lagName2), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "connections_bandwidth", "1Gbps"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "location", "EqSe2"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "number_of_connections", "2"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccAwsDxLag_tags(t *testing.T) { + lagName := fmt.Sprintf("tf-dx-lag-%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAwsDxLagDestroy, Steps: []resource.TestStep{ { - Config: testAccDxLagConfig(acctest.RandString(5)), + Config: testAccDxLagConfig_tags(lagName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsDxLagExists("aws_dx_lag.hoge"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "name", lagName), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.Usage", "original"), + ), + }, + { + Config: testAccDxLagConfig_tagsChanged(lagName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxLagExists("aws_dx_lag.hoge"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "name", lagName), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.Usage", "changed"), + ), + }, + { + Config: testAccDxLagConfig(lagName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsDxLagExists("aws_dx_lag.hoge"), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "name", lagName), + resource.TestCheckResourceAttr("aws_dx_lag.hoge", "tags.%", "0"), ), }, }, @@ -64,14 +121,47 @@ func testAccCheckAwsDxLagExists(name string) resource.TestCheckFunc { } } -func testAccDxLagConfig(rName string) string { +func testAccDxLagConfig(n string) string { return fmt.Sprintf(` - resource "aws_dx_lag" "hoge" { - name = "tf-dx-lag-%s" - connections_bandwidth = "1Gbps" - location = "EqSe2" - number_of_connections = 2 - force_destroy = true - } - `, rName) +resource "aws_dx_lag" "hoge" { + name = "%s" + connections_bandwidth = "1Gbps" + location = "EqSe2" + number_of_connections = 2 + force_destroy = true +} +`, n) +} + +func testAccDxLagConfig_tags(n string) string { + return fmt.Sprintf(` +resource "aws_dx_lag" "hoge" { + name = "%s" + connections_bandwidth = "1Gbps" + location = "EqSe2" + number_of_connections = 2 + force_destroy = true + + tags { + Environment = "production" + Usage = "original" + } +} +`, n) +} + +func testAccDxLagConfig_tagsChanged(n string) string { + return fmt.Sprintf(` +resource "aws_dx_lag" "hoge" { + name = "%s" + connections_bandwidth = "1Gbps" + location = "EqSe2" + number_of_connections = 2 + force_destroy = true + + tags { + Usage = "changed" + } +} +`, n) } diff --git a/aws/tagsDX.go b/aws/tagsDX.go new file mode 100644 index 00000000000..ca353a857e3 --- /dev/null +++ b/aws/tagsDX.go @@ -0,0 +1,137 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform/helper/schema" +) + +// getTags is a helper to get the tags for a resource. It expects the +// tags field to be named "tags" +func getTagsDX(conn *directconnect.DirectConnect, d *schema.ResourceData, arn string) error { + resp, err := conn.DescribeTags(&directconnect.DescribeTagsInput{ + ResourceArns: aws.StringSlice([]string{arn}), + }) + if err != nil { + return err + } + + var tags []*directconnect.Tag + if len(resp.ResourceTags) == 1 && aws.StringValue(resp.ResourceTags[0].ResourceArn) == arn { + tags = resp.ResourceTags[0].Tags + } + + if err := d.Set("tags", tagsToMapDX(tags)); err != nil { + return err + } + + return nil +} + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsDX(conn *directconnect.DirectConnect, d *schema.ResourceData, arn string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsDX(tagsFromMapDX(o), tagsFromMapDX(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, len(remove), len(remove)) + for i, t := range remove { + k[i] = t.Key + } + + _, err := conn.UntagResource(&directconnect.UntagResourceInput{ + ResourceArn: aws.String(arn), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.TagResource(&directconnect.TagResourceInput{ + ResourceArn: aws.String(arn), + 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 diffTagsDX(oldTags, newTags []*directconnect.Tag) ([]*directconnect.Tag, []*directconnect.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + // Build the list of what to remove + var remove []*directconnect.Tag + for _, t := range oldTags { + old, ok := create[aws.StringValue(t.Key)] + if !ok || old != aws.StringValue(t.Value) { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapDX(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapDX(m map[string]interface{}) []*directconnect.Tag { + result := make([]*directconnect.Tag, 0, len(m)) + for k, v := range m { + t := &directconnect.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + if !tagIgnoredDX(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapDX(ts []*directconnect.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredDX(t) { + result[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredDX(t *directconnect.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key) + if r, _ := regexp.MatchString(v, *t.Key); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value) + return true + } + } + return false +} diff --git a/aws/tagsDX_test.go b/aws/tagsDX_test.go new file mode 100644 index 00000000000..9af6d1b101d --- /dev/null +++ b/aws/tagsDX_test.go @@ -0,0 +1,79 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" +) + +// go test -v -run="TestDiffDXTags" +func TestDiffDXTags(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 := diffTagsDX(tagsFromMapDX(tc.Old), tagsFromMapDX(tc.New)) + cm := tagsToMapDX(c) + rm := tagsToMapDX(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) + } + } +} + +// go test -v -run="TestIgnoringTagsDX" +func TestIgnoringTagsDX(t *testing.T) { + var ignoredTags []*directconnect.Tag + ignoredTags = append(ignoredTags, &directconnect.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &directconnect.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredDX(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} diff --git a/website/docs/r/dx_connection.html.markdown b/website/docs/r/dx_connection.html.markdown index 32437c7e44e..49c47cee5a4 100644 --- a/website/docs/r/dx_connection.html.markdown +++ b/website/docs/r/dx_connection.html.markdown @@ -27,9 +27,11 @@ The following arguments are supported: * `name` - (Required) The name of the connection. * `bandwidth` - (Required) The bandwidth of the connection. Available values: 1Gbps, 10Gbps. Case sensitive. * `location` - (Required) The AWS Direct Connect location where the connection is located. See [DescribeLocations](https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeLocations.html) for the list of AWS Direct Connect locations. Use `locationCode`. +* `tags` - (Optional) A mapping of tags to assign to the resource. ## Attributes Reference The following attributes are exported: * `id` - The ID of the connection. +* `arn` - The ARN of the connection. diff --git a/website/docs/r/dx_lag.html.markdown b/website/docs/r/dx_lag.html.markdown index fdae2135322..27c91d745c5 100644 --- a/website/docs/r/dx_lag.html.markdown +++ b/website/docs/r/dx_lag.html.markdown @@ -2,7 +2,7 @@ layout: "aws" page_title: "AWS: aws_dx_lag" sidebar_current: "docs-aws-resource-dx-lag" -description: |- +description: |- Provides a Direct Connect LAG. --- @@ -31,9 +31,11 @@ The following arguments are supported: * `location` - (Required) The AWS Direct Connect location in which the LAG should be allocated. See [DescribeLocations](https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeLocations.html) for the list of AWS Direct Connect locations. Use `locationCode`. * `number_of_connections` - (Required) The number of physical connections initially provisioned and bundled by the LAG. * `force_destroy` - (Optional, Default:false) A boolean that indicates all connections associated with the LAG should be deleted so that the LAG can be destroyed without error. These objects are *not* recoverable. +* `tags` - (Optional) A mapping of tags to assign to the resource. ## Attributes Reference The following attributes are exported: * `id` - The ID of the LAG. +* `arn` - The ARN of the LAG.