diff --git a/aws/resource_aws_lightsail_instance.go b/aws/resource_aws_lightsail_instance.go index 17fdf90e603..b35c7c55d8d 100644 --- a/aws/resource_aws_lightsail_instance.go +++ b/aws/resource_aws_lightsail_instance.go @@ -17,6 +17,7 @@ func resourceAwsLightsailInstance() *schema.Resource { return &schema.Resource{ Create: resourceAwsLightsailInstanceCreate, Read: resourceAwsLightsailInstanceRead, + Update: resourceAwsLightsailInstanceUpdate, Delete: resourceAwsLightsailInstanceDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, @@ -103,6 +104,7 @@ func resourceAwsLightsailInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "tags": tagsSchema(), }, } } @@ -126,6 +128,12 @@ func resourceAwsLightsailInstanceCreate(d *schema.ResourceData, meta interface{} req.UserData = aws.String(v.(string)) } + tags := tagsFromMapLightsail(d.Get("tags").(map[string]interface{})) + + if len(tags) != 0 { + req.Tags = tags + } + resp, err := conn.CreateInstances(&req) if err != nil { return err @@ -199,6 +207,10 @@ func resourceAwsLightsailInstanceRead(d *schema.ResourceData, meta interface{}) d.Set("private_ip_address", i.PrivateIpAddress) d.Set("public_ip_address", i.PublicIpAddress) + if err := d.Set("tags", tagsToMapLightsail(i.Tags)); err != nil { + return fmt.Errorf("Error setting tags: %s", err) + } + return nil } @@ -233,6 +245,19 @@ func resourceAwsLightsailInstanceDelete(d *schema.ResourceData, meta interface{} return nil } +func resourceAwsLightsailInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lightsailconn + + if d.HasChange("tags") { + if err := setTagsLightsail(conn, d); err != nil { + return err + } + d.SetPartial("tags") + } + + return resourceAwsLightsailInstanceRead(d, meta) +} + // method to check the status of an Operation, which is returned from // Create/Delete methods. // Status's are an aws.OperationStatus enum: diff --git a/aws/resource_aws_lightsail_instance_test.go b/aws/resource_aws_lightsail_instance_test.go index 938f198134d..324cdbf7cb9 100644 --- a/aws/resource_aws_lightsail_instance_test.go +++ b/aws/resource_aws_lightsail_instance_test.go @@ -32,6 +32,7 @@ func TestAccAWSLightsailInstance_basic(t *testing.T) { resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "blueprint_id"), resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "bundle_id"), resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "key_pair_name"), + resource.TestCheckResourceAttr("aws_lightsail_instance.lightsail_instance_test", "tags.%", "0"), ), }, }, @@ -62,6 +63,42 @@ func TestAccAWSLightsailInstance_euRegion(t *testing.T) { }) } +func TestAccAWSLightsailInstance_update(t *testing.T) { + var conf lightsail.Instance + lightsailName := fmt.Sprintf("tf-test-lightsail-%d", acctest.RandInt()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSLightsail(t) }, + IDRefreshName: "aws_lightsail_instance.lightsail_instance_test", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLightsailInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLightsailInstanceConfig_update(lightsailName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailInstanceExists("aws_lightsail_instance.lightsail_instance_test", &conf), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "availability_zone"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "blueprint_id"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "bundle_id"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "key_pair_name"), + resource.TestCheckResourceAttr("aws_lightsail_instance.lightsail_instance_test", "tags.%", "1"), + ), + }, + { + Config: testAccAWSLightsailInstanceConfig_updated(lightsailName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLightsailInstanceExists("aws_lightsail_instance.lightsail_instance_test", &conf), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "availability_zone"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "blueprint_id"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "bundle_id"), + resource.TestCheckResourceAttrSet("aws_lightsail_instance.lightsail_instance_test", "key_pair_name"), + resource.TestCheckResourceAttr("aws_lightsail_instance.lightsail_instance_test", "tags.%", "2"), + ), + }, + }, + }) +} + func TestAccAWSLightsailInstance_disapear(t *testing.T) { var conf lightsail.Instance lightsailName := fmt.Sprintf("tf-test-lightsail-%d", acctest.RandInt()) @@ -191,6 +228,43 @@ resource "aws_lightsail_instance" "lightsail_instance_test" { `, lightsailName) } +func testAccAWSLightsailInstanceConfig_update(lightsailName string) string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" { + state = "available" +} + +resource "aws_lightsail_instance" "lightsail_instance_test" { + name = "%s" + availability_zone = "${data.aws_availability_zones.available.names[0]}" + blueprint_id = "amazon_linux" + bundle_id = "nano_1_0" + tags = { + Name = "tf-test" + } +} +`, lightsailName) +} + +func testAccAWSLightsailInstanceConfig_updated(lightsailName string) string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" { + state = "available" +} + +resource "aws_lightsail_instance" "lightsail_instance_test" { + name = "%s" + availability_zone = "${data.aws_availability_zones.available.names[0]}" + blueprint_id = "amazon_linux" + bundle_id = "nano_1_0" + tags = { + Name = "tf-test", + ExtraName = "tf-test" + } +} +`, lightsailName) +} + func testAccAWSLightsailInstanceConfig_euRegion(lightsailName string) string { return fmt.Sprintf(` provider "aws" { diff --git a/aws/tagsLightsail.go b/aws/tagsLightsail.go new file mode 100644 index 00000000000..cecbcb8047a --- /dev/null +++ b/aws/tagsLightsail.go @@ -0,0 +1,97 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/hashicorp/terraform/helper/schema" +) + +// 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 diffTagsLightsail(oldTags, newTags []*lightsail.Tag) ([]*lightsail.Tag, []*lightsail.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 []*lightsail.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) + } else if ok { + delete(create, aws.StringValue(t.Key)) + } + } + + return tagsFromMapLightsail(create), remove +} + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsLightsail(conn *lightsail.Lightsail, 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 := diffTagsLightsail(tagsFromMapLightsail(o), tagsFromMapLightsail(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, len(remove)) + for i, t := range remove { + k[i] = t.Key + } + + _, err := conn.UntagResource(&lightsail.UntagResourceInput{ + ResourceName: aws.String(d.Get("name").(string)), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.TagResource(&lightsail.TagResourceInput{ + ResourceName: aws.String(d.Get("name").(string)), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapLightsail(m map[string]interface{}) []*lightsail.Tag { + result := make([]*lightsail.Tag, 0, len(m)) + for k, v := range m { + result = append(result, &lightsail.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapLightsail(ts []*lightsail.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + return result +} diff --git a/aws/tagsLightsail_test.go b/aws/tagsLightsail_test.go new file mode 100644 index 00000000000..e448efe6389 --- /dev/null +++ b/aws/tagsLightsail_test.go @@ -0,0 +1,91 @@ +package aws + +import ( + "reflect" + "testing" +) + +// go test -v -run="TestDiffLightsailTags" +func TestDiffLightsailTags(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", + }, + }, + + // Overlap + { + Old: map[string]interface{}{ + "foo": "bar", + "hello": "world", + }, + New: map[string]interface{}{ + "foo": "baz", + "hello": "world", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Remove + { + Old: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + New: map[string]interface{}{ + "foo": "bar", + }, + Create: map[string]string{}, + Remove: map[string]string{ + "bar": "baz", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsLightsail(tagsFromMapLightsail(tc.Old), tagsFromMapLightsail(tc.New)) + cm := tagsToMapLightsail(c) + rm := tagsToMapLightsail(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) + } + } +} diff --git a/website/docs/r/lightsail_instance.html.markdown b/website/docs/r/lightsail_instance.html.markdown index 9352e4486b1..19716838463 100644 --- a/website/docs/r/lightsail_instance.html.markdown +++ b/website/docs/r/lightsail_instance.html.markdown @@ -24,6 +24,9 @@ resource "aws_lightsail_instance" "gitlab_test" { blueprint_id = "string" bundle_id = "string" key_pair_name = "some_key_name" + tags = { + foo = "bar" + } } ``` @@ -40,6 +43,7 @@ instance (see list below) * `key_pair_name` - (Optional) The name of your key pair. Created in the Lightsail console (cannot use `aws_key_pair` at this time) * `user_data` - (Optional) launch script to configure server with additional user data +* `tags` - (Optional) A mapping of tags to assign to the resource. ## Availability Zones Lightsail currently supports the following Availability Zones (e.g. `us-east-1a`):