diff --git a/aws/provider.go b/aws/provider.go index 3f328c6761b..b46a13e9fc3 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -407,6 +407,7 @@ func Provider() terraform.ResourceProvider { "aws_internet_gateway": resourceAwsInternetGateway(), "aws_iot_certificate": resourceAwsIotCertificate(), "aws_iot_policy": resourceAwsIotPolicy(), + "aws_iot_thing": resourceAwsIotThing(), "aws_iot_thing_type": resourceAwsIotThingType(), "aws_iot_topic_rule": resourceAwsIotTopicRule(), "aws_key_pair": resourceAwsKeyPair(), diff --git a/aws/resource_aws_iot_thing.go b/aws/resource_aws_iot_thing.go new file mode 100644 index 00000000000..3fef149f88e --- /dev/null +++ b/aws/resource_aws_iot_thing.go @@ -0,0 +1,162 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsIotThing() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIotThingCreate, + Read: resourceAwsIotThingRead, + Update: resourceAwsIotThingUpdate, + Delete: resourceAwsIotThingDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "attributes": { + Type: schema.TypeMap, + Optional: true, + }, + "thing_type_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "default_client_id": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsIotThingCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.CreateThingInput{ + ThingName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("thing_type_name"); ok { + params.ThingTypeName = aws.String(v.(string)) + } + if v, ok := d.GetOk("attributes"); ok { + params.AttributePayload = &iot.AttributePayload{ + Attributes: stringMapToPointers(v.(map[string]interface{})), + } + } + + log.Printf("[DEBUG] Creating IoT Thing: %s", params) + out, err := conn.CreateThing(params) + if err != nil { + return err + } + + d.SetId(*out.ThingName) + + return resourceAwsIotThingRead(d, meta) +} + +func resourceAwsIotThingRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.DescribeThingInput{ + ThingName: aws.String(d.Id()), + } + log.Printf("[DEBUG] Reading IoT Thing: %s", params) + out, err := conn.DescribeThing(params) + + if err != nil { + if isAWSErr(err, iot.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] IoT Thing %q not found, removing from state", d.Id()) + d.SetId("") + } + return err + } + + log.Printf("[DEBUG] Received IoT Thing: %s", out) + + d.Set("arn", out.ThingArn) + d.Set("name", out.ThingName) + d.Set("attributes", aws.StringValueMap(out.Attributes)) + d.Set("default_client_id", out.DefaultClientId) + d.Set("thing_type_name", out.ThingTypeName) + d.Set("version", out.Version) + + return nil +} + +func resourceAwsIotThingUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.UpdateThingInput{ + ThingName: aws.String(d.Get("name").(string)), + } + if d.HasChange("thing_type_name") { + if v, ok := d.GetOk("thing_type_name"); ok { + params.ThingTypeName = aws.String(v.(string)) + } else { + params.RemoveThingType = aws.Bool(true) + } + } + if d.HasChange("attributes") { + attributes := map[string]*string{} + + if v, ok := d.GetOk("attributes"); ok { + if m, ok := v.(map[string]interface{}); ok { + attributes = stringMapToPointers(m) + } + } + params.AttributePayload = &iot.AttributePayload{ + Attributes: attributes, + } + } + + _, err := conn.UpdateThing(params) + if err != nil { + return err + } + + return resourceAwsIotThingRead(d, meta) +} + +func resourceAwsIotThingDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.DeleteThingInput{ + ThingName: aws.String(d.Id()), + } + log.Printf("[DEBUG] Deleting IoT Thing: %s", params) + + _, err := conn.DeleteThing(params) + if err != nil { + if isAWSErr(err, iot.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + + return nil +} diff --git a/aws/resource_aws_iot_thing_test.go b/aws/resource_aws_iot_thing_test.go new file mode 100644 index 00000000000..97e9b94b180 --- /dev/null +++ b/aws/resource_aws_iot_thing_test.go @@ -0,0 +1,194 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSIotThing_basic(t *testing.T) { + var thing iot.DescribeThingOutput + rString := acctest.RandString(8) + thingName := fmt.Sprintf("tf_acc_thing_%s", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingConfig_basic(thingName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIotThingExists("aws_iot_thing.test", &thing), + resource.TestCheckResourceAttr("aws_iot_thing.test", "name", thingName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.%", "0"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "thing_type_name", ""), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "arn"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "default_client_id"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "version"), + ), + }, + }, + }) +} + +func TestAccAWSIotThing_full(t *testing.T) { + var thing iot.DescribeThingOutput + rString := acctest.RandString(8) + thingName := fmt.Sprintf("tf_acc_thing_%s", rString) + typeName := fmt.Sprintf("tf_acc_type_%s", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingConfig_full(thingName, typeName, "42"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIotThingExists("aws_iot_thing.test", &thing), + resource.TestCheckResourceAttr("aws_iot_thing.test", "name", thingName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "thing_type_name", typeName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.%", "3"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.One", "11111"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.Two", "TwoTwo"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.Answer", "42"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "arn"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "default_client_id"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "version"), + ), + }, + { // Update attribute + Config: testAccAWSIotThingConfig_full(thingName, typeName, "differentOne"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIotThingExists("aws_iot_thing.test", &thing), + resource.TestCheckResourceAttr("aws_iot_thing.test", "name", thingName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "thing_type_name", typeName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.%", "3"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.One", "11111"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.Two", "TwoTwo"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.Answer", "differentOne"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "arn"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "default_client_id"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "version"), + ), + }, + { // Remove thing type association + Config: testAccAWSIotThingConfig_basic(thingName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIotThingExists("aws_iot_thing.test", &thing), + resource.TestCheckResourceAttr("aws_iot_thing.test", "name", thingName), + resource.TestCheckResourceAttr("aws_iot_thing.test", "attributes.%", "0"), + resource.TestCheckResourceAttr("aws_iot_thing.test", "thing_type_name", ""), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "arn"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "default_client_id"), + resource.TestCheckResourceAttrSet("aws_iot_thing.test", "version"), + ), + }, + }, + }) +} + +func TestAccAWSIotThing_importBasic(t *testing.T) { + resourceName := "aws_iot_thing.test" + rString := acctest.RandString(8) + thingName := fmt.Sprintf("tf_acc_thing_%s", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingConfig_basic(thingName), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckIotThingExists(n string, thing *iot.DescribeThingOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No IoT Thing ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).iotconn + params := &iot.DescribeThingInput{ + ThingName: aws.String(rs.Primary.ID), + } + resp, err := conn.DescribeThing(params) + if err != nil { + return err + } + + *thing = *resp + + return nil + } +} + +func testAccCheckAWSIotThingDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).iotconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iot_thing" { + continue + } + + params := &iot.DescribeThingInput{ + ThingName: aws.String(rs.Primary.ID), + } + + _, err := conn.DescribeThing(params) + if err != nil { + if isAWSErr(err, iot.ErrCodeResourceNotFoundException, "") { + return nil + } + } + return fmt.Errorf("Expected IoT Thing to be destroyed, %s found", rs.Primary.ID) + + } + + return nil +} + +func testAccAWSIotThingConfig_basic(thingName string) string { + return fmt.Sprintf(` +resource "aws_iot_thing" "test" { + name = "%s" +} +`, thingName) +} + +func testAccAWSIotThingConfig_full(thingName, typeName, answer string) string { + return fmt.Sprintf(` +resource "aws_iot_thing" "test" { + name = "%s" + attributes { + One = "11111" + Two = "TwoTwo" + Answer = "%s" + } + thing_type_name = "${aws_iot_thing_type.test.name}" +} + +resource "aws_iot_thing_type" "test" { + name = "%s" +} +`, thingName, answer, typeName) +} diff --git a/website/aws.erb b/website/aws.erb index 235f8f196c1..85e79d48653 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1167,6 +1167,9 @@