From 9427513366c1664eb81ceed62c253167f133d39d Mon Sep 17 00:00:00 2001 From: Ninir Date: Fri, 9 Feb 2018 00:17:26 +0100 Subject: [PATCH] New Resource: aws_iot_thing_type --- aws/import_aws_iot_thing_type_test.go | 29 +++ aws/provider.go | 1 + aws/resource_aws_iot_thing_type.go | 202 ++++++++++++++++++++ aws/resource_aws_iot_thing_type_test.go | 116 +++++++++++ aws/structure.go | 27 +++ aws/validators.go | 35 ++++ website/aws.erb | 3 + website/docs/r/iot_thing_type.html.markdown | 33 ++++ 8 files changed, 446 insertions(+) create mode 100644 aws/import_aws_iot_thing_type_test.go create mode 100644 aws/resource_aws_iot_thing_type.go create mode 100644 aws/resource_aws_iot_thing_type_test.go create mode 100644 website/docs/r/iot_thing_type.html.markdown diff --git a/aws/import_aws_iot_thing_type_test.go b/aws/import_aws_iot_thing_type_test.go new file mode 100644 index 000000000000..026e734e983c --- /dev/null +++ b/aws/import_aws_iot_thing_type_test.go @@ -0,0 +1,29 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAWSIotThingType_importBasic(t *testing.T) { + resourceName := "aws_iot_thing_type.foo" + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingTypeConfig_basic(rInt), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/aws/provider.go b/aws/provider.go index a00cb450f184..a73de8cb91e8 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_type": resourceAwsIotThingType(), "aws_iot_topic_rule": resourceAwsIotTopicRule(), "aws_key_pair": resourceAwsKeyPair(), "aws_kinesis_firehose_delivery_stream": resourceAwsKinesisFirehoseDeliveryStream(), diff --git a/aws/resource_aws_iot_thing_type.go b/aws/resource_aws_iot_thing_type.go new file mode 100644 index 000000000000..5aa60465d89b --- /dev/null +++ b/aws/resource_aws_iot_thing_type.go @@ -0,0 +1,202 @@ +package aws + +import ( + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +// https://docs.aws.amazon.com/iot/latest/apireference/API_CreateThingType.html +func resourceAwsIotThingType() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIotThingTypeCreate, + Read: resourceAwsIotThingTypeRead, + Update: resourceAwsIotThingTypeUpdate, + Delete: resourceAwsIotThingTypeDelete, + + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("name", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateIotThingTypeName, + }, + "properties": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateIotThingTypeDescription, + }, + "searchable_attributes": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + MaxItems: 3, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateIotThingTypeSearchableAttribute, + }, + }, + }, + }, + }, + "deprecated": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsIotThingTypeCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.CreateThingTypeInput{ + ThingTypeName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("properties"); ok { + configs := v.([]interface{}) + config, ok := configs[0].(map[string]interface{}) + + if ok && config != nil { + params.ThingTypeProperties = expandIotThingTypeProperties(config) + } + } + + log.Printf("[DEBUG] Creating IoT Thing Type: %s", params) + out, err := conn.CreateThingType(params) + + if err != nil { + return err + } + + d.SetId(*out.ThingTypeName) + + if v := d.Get("deprecated").(bool); v { + params := &iot.DeprecateThingTypeInput{ + ThingTypeName: aws.String(d.Id()), + UndoDeprecate: aws.Bool(false), + } + + log.Printf("[DEBUG] Deprecating IoT Thing Type: %s", params) + _, err := conn.DeprecateThingType(params) + + if err != nil { + return err + } + } + + return resourceAwsIotThingTypeRead(d, meta) +} + +func resourceAwsIotThingTypeRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + params := &iot.DescribeThingTypeInput{ + ThingTypeName: aws.String(d.Id()), + } + log.Printf("[DEBUG] Reading IoT Thing Type: %s", params) + out, err := conn.DescribeThingType(params) + + if err != nil { + if isAWSErr(err, iot.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] IoT Thing Type %q not found, removing from state", d.Id()) + d.SetId("") + } + return err + } + + if out.ThingTypeMetadata != nil { + d.Set("deprecated", out.ThingTypeMetadata.Deprecated) + } + + d.Set("arn", out.ThingTypeArn) + d.Set("properties", flattenIotThingTypeProperties(out.ThingTypeProperties)) + + return nil +} + +func resourceAwsIotThingTypeUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + if d.HasChange("deprecated") { + params := &iot.DeprecateThingTypeInput{ + ThingTypeName: aws.String(d.Id()), + UndoDeprecate: aws.Bool(!d.Get("deprecated").(bool)), + } + + log.Printf("[DEBUG] Updating IoT Thing Type: %s", params) + _, err := conn.DeprecateThingType(params) + + if err != nil { + return err + } + } + + return resourceAwsIotThingTypeRead(d, meta) +} + +func resourceAwsIotThingTypeDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).iotconn + + // In order to delete an IoT Thing Type, you must deprecate it first and wait + // at least 5 minutes. + deprecateParams := &iot.DeprecateThingTypeInput{ + ThingTypeName: aws.String(d.Id()), + } + log.Printf("[DEBUG] Deprecating IoT Thing Type: %s", deprecateParams) + _, err := conn.DeprecateThingType(deprecateParams) + + if err != nil { + return err + } + + deleteParams := &iot.DeleteThingTypeInput{ + ThingTypeName: aws.String(d.Id()), + } + log.Printf("[DEBUG] Deleting IoT Thing Type: %s", deleteParams) + + return resource.Retry(6*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteThingType(deleteParams) + + if err != nil { + if isAWSErr(err, iot.ErrCodeInvalidRequestException, "Please wait for 5 minutes after deprecation and then retry") { + return resource.RetryableError(err) + } + + // As the delay post-deprecation is about 5 minutes, it may have been + // deleted in between, thus getting a Not Found Exception. + if isAWSErr(err, iot.ErrCodeResourceNotFoundException, "") { + return nil + } + + return resource.NonRetryableError(err) + } + + return nil + }) +} diff --git a/aws/resource_aws_iot_thing_type_test.go b/aws/resource_aws_iot_thing_type_test.go new file mode 100644 index 000000000000..4d2526243890 --- /dev/null +++ b/aws/resource_aws_iot_thing_type_test.go @@ -0,0 +1,116 @@ +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 TestAccAWSIotThingType_basic(t *testing.T) { + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingTypeConfig_basic(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("aws_iot_thing_type.foo", "arn"), + resource.TestCheckResourceAttr("aws_iot_thing_type.foo", "name", fmt.Sprintf("tf_acc_iot_thing_type_%d", rInt)), + ), + }, + }, + }) +} + +func TestAccAWSIotThingType_full(t *testing.T) { + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIotThingTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIotThingTypeConfig_full(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("aws_iot_thing_type.foo", "arn"), + resource.TestCheckResourceAttr("aws_iot_thing_type.foo", "properties.0.description", "MyDescription"), + resource.TestCheckResourceAttr("aws_iot_thing_type.foo", "properties.0.searchable_attributes.#", "3"), + resource.TestCheckResourceAttr("aws_iot_thing_type.foo", "deprecated", "true"), + ), + }, + { + Config: testAccAWSIotThingTypeConfig_fullUpdated(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_iot_thing_type.foo", "deprecated", "false"), + ), + }, + }, + }) +} + +func testAccCheckAWSIotThingTypeDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).iotconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iot_thing_type" { + continue + } + + params := &iot.DescribeThingTypeInput{ + ThingTypeName: aws.String(rs.Primary.ID), + } + + _, err := conn.DescribeThingType(params) + if err == nil { + return fmt.Errorf("Expected IoT Thing Type to be destroyed, %s found", rs.Primary.ID) + } + + } + + return nil +} + +func testAccAWSIotThingTypeConfig_basic(rName int) string { + return fmt.Sprintf(` +resource "aws_iot_thing_type" "foo" { + name = "tf_acc_iot_thing_type_%d" +} +`, rName) +} + +func testAccAWSIotThingTypeConfig_full(rName int) string { + return fmt.Sprintf(` +resource "aws_iot_thing_type" "foo" { + name = "tf_acc_iot_thing_type_%d" + deprecated = true + + properties { + description = "MyDescription" + searchable_attributes = ["foo", "bar", "baz"] + } +} +`, rName) +} + +func testAccAWSIotThingTypeConfig_fullUpdated(rName int) string { + return fmt.Sprintf(` +resource "aws_iot_thing_type" "foo" { + name = "tf_acc_iot_thing_type_%d" + deprecated = false + + properties { + description = "MyDescription" + searchable_attributes = ["foo", "bar", "baz"] + } +} +`, rName) +} diff --git a/aws/structure.go b/aws/structure.go index df20bf0e3279..39a0ad00db58 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -3678,3 +3678,30 @@ func flattenDynamoDbTableItemAttributes(attrs map[string]*dynamodb.AttributeValu return rawBuffer.String(), nil } + +func expandIotThingTypeProperties(config map[string]interface{}) *iot.ThingTypeProperties { + properties := &iot.ThingTypeProperties{ + SearchableAttributes: expandStringList(config["searchable_attributes"].(*schema.Set).List()), + } + + if v, ok := config["description"]; ok && v.(string) != "" { + properties.ThingTypeDescription = aws.String(v.(string)) + } + + return properties +} + +func flattenIotThingTypeProperties(s *iot.ThingTypeProperties) []map[string]interface{} { + m := map[string]interface{}{} + + if s == nil { + return nil + } + + if s.ThingTypeDescription != nil { + m["description"] = *s.ThingTypeDescription + } + m["searchable_attributes"] = flattenStringList(s.SearchableAttributes) + + return []map[string]interface{}{m} +} diff --git a/aws/validators.go b/aws/validators.go index 331bd23ab823..3199e3229e9e 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -2257,3 +2257,38 @@ func validateAmazonSideAsn(v interface{}, k string) (ws []string, errors []error } return } + +func validateIotThingTypeName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`[a-zA-Z0-9:_-]+`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only alphanumeric characters, colons, underscores and hyphens allowed in %q", k)) + } + return +} + +func validateIotThingTypeDescription(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 2028 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 2028 characters", k)) + } + if !regexp.MustCompile(`[\\p{Graph}\\x20]*`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q must match pattern [\\p{Graph}\\x20]*", k)) + } + return +} + +func validateIotThingTypeSearchableAttribute(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 128 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 128 characters", k)) + } + if !regexp.MustCompile(`[a-zA-Z0-9_.,@/:#-]+`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only alphanumeric characters, underscores, dots, commas, arobases, slashes, colons, hashes and hyphens allowed in %q", k)) + } + return +} diff --git a/website/aws.erb b/website/aws.erb index 1779231c1dc9..73b89e00f756 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1159,6 +1159,9 @@ > aws_iot_topic_rule + > + aws_iot_thing_type + diff --git a/website/docs/r/iot_thing_type.html.markdown b/website/docs/r/iot_thing_type.html.markdown new file mode 100644 index 000000000000..2a5829bd9367 --- /dev/null +++ b/website/docs/r/iot_thing_type.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "aws" +page_title: "AWS: aws_iot_thing_type" +sidebar_current: "docs-aws-resource-iot-thing-type" +description: |- + Creates and manages an AWS IoT Thing Type. +--- + +# aws_iot_thing_type + +Creates and manages an AWS IoT Thing Type. + +## Example Usage + +```hcl +resource "aws_iot_thing_type" "foo" { + name = "my_iot_thing" +} +``` + +## Argument Reference + +* `name` - (Required, Forces New Resource) The name of the thing type. +* `description` - (Optional, Forces New Resource) The description of the thing type. +* `deprecated` - (Optional, Defaults to false) Whether the thing type is deprecated. If true, no new things could be associated with this type. +* `searchable_attributes` - (Optional, Forces New Resource) A list of searchable thing attribute names. + + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `arn` - The ARN of the created AWS IoT Thing Type.