diff --git a/.changelog/12108.txt b/.changelog/12108.txt new file mode 100644 index 00000000000..f210a4a379e --- /dev/null +++ b/.changelog/12108.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_iot_provisioning_template +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f8d7051c6db..192872cdbce 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1510,6 +1510,7 @@ func Provider() *schema.Provider { "aws_iot_certificate": iot.ResourceCertificate(), "aws_iot_policy": iot.ResourcePolicy(), "aws_iot_policy_attachment": iot.ResourcePolicyAttachment(), + "aws_iot_provisioning_template": iot.ResourceProvisioningTemplate(), "aws_iot_role_alias": iot.ResourceRoleAlias(), "aws_iot_thing": iot.ResourceThing(), "aws_iot_thing_group": iot.ResourceThingGroup(), diff --git a/internal/service/backup/report_plan.go b/internal/service/backup/report_plan.go index 30a71de62d4..b4a6854f4a7 100644 --- a/internal/service/backup/report_plan.go +++ b/internal/service/backup/report_plan.go @@ -360,6 +360,10 @@ func FindReportPlanByName(conn *backup.Backup, name string) (*backup.ReportPlan, } } + if err != nil { + return nil, err + } + if output == nil || output.ReportPlan == nil { return nil, tfresource.NewEmptyResultError(input) } diff --git a/internal/service/iot/provisioning_template.go b/internal/service/iot/provisioning_template.go new file mode 100644 index 00000000000..009cb03c089 --- /dev/null +++ b/internal/service/iot/provisioning_template.go @@ -0,0 +1,338 @@ +package iot + +import ( + "context" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +const ( + provisioningHookPayloadVersion2020_04_01 = "2020-04-01" +) + +func provisioningHookPayloadVersion_Values() []string { + return []string{ + provisioningHookPayloadVersion2020_04_01, + } +} + +func ResourceProvisioningTemplate() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceProvisioningTemplateCreate, + ReadWithoutTimeout: resourceProvisioningTemplateRead, + UpdateWithoutTimeout: resourceProvisioningTemplateUpdate, + DeleteWithoutTimeout: resourceProvisioningTemplateDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "default_version_id": { + Type: schema.TypeInt, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 500), + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 36), + validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z_-]+$`), "must contain only alphanumeric characters and/or the following: _-"), + ), + }, + "pre_provisioning_hook": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "payload_version": { + Type: schema.TypeString, + Optional: true, + Default: provisioningHookPayloadVersion2020_04_01, + ValidateFunc: validation.StringInSlice(provisioningHookPayloadVersion_Values(), false), + }, + "target_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, + "provisioning_role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + "template_body": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringIsJSON, + validation.StringLenBetween(0, 10240), + ), + }, + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceProvisioningTemplateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IoTConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("name").(string) + input := &iot.CreateProvisioningTemplateInput{ + Enabled: aws.Bool(d.Get("enabled").(bool)), + TemplateName: aws.String(name), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("pre_provisioning_hook"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.PreProvisioningHook = expandProvisioningHook(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("provisioning_role_arn"); ok { + input.ProvisioningRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("template_body"); ok { + input.TemplateBody = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + log.Printf("[DEBUG] Creating IoT Provisioning Template: %s", input) + outputRaw, err := tfresource.RetryWhenAWSErrMessageContainsContext(ctx, tfiam.PropagationTimeout, + func() (interface{}, error) { + return conn.CreateProvisioningTemplateWithContext(ctx, input) + }, + iot.ErrCodeInvalidRequestException, "The provisioning role cannot be assumed by AWS IoT") + + if err != nil { + return diag.Errorf("error creating IoT Provisioning Template (%s): %s", name, err) + } + + d.SetId(aws.StringValue(outputRaw.(*iot.CreateProvisioningTemplateOutput).TemplateName)) + + return resourceProvisioningTemplateRead(ctx, d, meta) +} + +func resourceProvisioningTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IoTConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + output, err := FindProvisioningTemplateByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] IoT Provisioning Template %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading IoT Provisioning Template (%s): %s", d.Id(), err) + } + + d.Set("arn", output.TemplateArn) + d.Set("default_version_id", output.DefaultVersionId) + d.Set("description", output.Description) + d.Set("enabled", output.Enabled) + d.Set("name", output.TemplateName) + if output.PreProvisioningHook != nil { + if err := d.Set("pre_provisioning_hook", []interface{}{flattenProvisioningHook(output.PreProvisioningHook)}); err != nil { + return diag.Errorf("error setting pre_provisioning_hook: %s", err) + } + } else { + d.Set("pre_provisioning_hook", nil) + } + d.Set("provisioning_role_arn", output.ProvisioningRoleArn) + d.Set("template_body", output.TemplateBody) + + tags, err := ListTags(conn, d.Get("arn").(string)) + + if err != nil { + return diag.Errorf("error listing tags for IoT Provisioning Template (%s): %s", d.Id(), err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("error setting tags_all: %s", err) + } + + return nil +} + +func resourceProvisioningTemplateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IoTConn + + if d.HasChange("template_body") { + input := &iot.CreateProvisioningTemplateVersionInput{ + SetAsDefault: aws.Bool(true), + TemplateBody: aws.String(d.Get("template_body").(string)), + TemplateName: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Creating IoT Provisioning Template version: %s", input) + _, err := conn.CreateProvisioningTemplateVersionWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error creating IoT Provisioning Template (%s) version: %s", d.Id(), err) + } + } + + if d.HasChanges("description", "enabled", "provisioning_role_arn") { + input := &iot.UpdateProvisioningTemplateInput{ + Description: aws.String(d.Get("description").(string)), + Enabled: aws.Bool(d.Get("enabled").(bool)), + ProvisioningRoleArn: aws.String(d.Get("provisioning_role_arn").(string)), + TemplateName: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Updating IoT Provisioning Template: %s", input) + _, err := tfresource.RetryWhenAWSErrMessageContainsContext(ctx, tfiam.PropagationTimeout, + func() (interface{}, error) { + return conn.UpdateProvisioningTemplateWithContext(ctx, input) + }, + iot.ErrCodeInvalidRequestException, "The provisioning role cannot be assumed by AWS IoT") + + if err != nil { + return diag.Errorf("error updating IoT Provisioning Template (%s): %s", d.Id(), err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("error updating tags: %s", err) + } + } + + return resourceProvisioningTemplateRead(ctx, d, meta) +} + +func resourceProvisioningTemplateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IoTConn + + log.Printf("[INFO] Deleting IoT Provisioning Template: %s", d.Id()) + _, err := conn.DeleteProvisioningTemplateWithContext(ctx, &iot.DeleteProvisioningTemplateInput{ + TemplateName: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, iot.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("error deleting IoT Provisioning Template (%s): %s", d.Id(), err) + } + + return nil +} + +func flattenProvisioningHook(apiObject *iot.ProvisioningHook) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.PayloadVersion; v != nil { + tfMap["payload_version"] = aws.StringValue(v) + } + + if v := apiObject.TargetArn; v != nil { + tfMap["target_arn"] = aws.StringValue(v) + } + + return tfMap +} + +func expandProvisioningHook(tfMap map[string]interface{}) *iot.ProvisioningHook { + if tfMap == nil { + return nil + } + + apiObject := &iot.ProvisioningHook{} + + if v, ok := tfMap["payload_version"].(string); ok && v != "" { + apiObject.PayloadVersion = aws.String(v) + } + + if v, ok := tfMap["target_arn"].(string); ok && v != "" { + apiObject.TargetArn = aws.String(v) + } + + return apiObject +} + +func FindProvisioningTemplateByName(ctx context.Context, conn *iot.IoT, name string) (*iot.DescribeProvisioningTemplateOutput, error) { + input := &iot.DescribeProvisioningTemplateInput{ + TemplateName: aws.String(name), + } + + output, err := conn.DescribeProvisioningTemplateWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, iot.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} diff --git a/internal/service/iot/provisioning_template_test.go b/internal/service/iot/provisioning_template_test.go new file mode 100644 index 00000000000..b441ac3e85b --- /dev/null +++ b/internal/service/iot/provisioning_template_test.go @@ -0,0 +1,424 @@ +package iot_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfiot "github.com/hashicorp/terraform-provider-aws/internal/service/iot" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccIoTProvisioningTemplate_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iot_provisioning_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iot.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckProvisioningTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProvisioningTemplateConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + testAccCheckProvisioningTemplateNumVersions(rName, 1), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "pre_provisioning_hook.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "provisioning_role_arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "template_body"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccIoTProvisioningTemplate_disappears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iot_provisioning_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iot.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckProvisioningTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProvisioningTemplateConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfiot.ResourceProvisioningTemplate(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccIoTProvisioningTemplate_tags(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iot_provisioning_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iot.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckProvisioningTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProvisioningTemplateConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + testAccCheckProvisioningTemplateNumVersions(rName, 1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProvisioningTemplateConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + testAccCheckProvisioningTemplateNumVersions(rName, 1), + ), + }, + { + Config: testAccProvisioningTemplateConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + testAccCheckProvisioningTemplateNumVersions(rName, 1), + ), + }, + }, + }) +} + +func TestAccIoTProvisioningTemplate_update(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iot_provisioning_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iot.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckProvisioningTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProvisioningTemplateConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + testAccCheckProvisioningTemplateNumVersions(rName, 1), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "pre_provisioning_hook.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "provisioning_role_arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "template_body"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProvisioningTemplateUpdatedConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProvisioningTemplateExists(resourceName), + testAccCheckProvisioningTemplateNumVersions(rName, 2), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", "For testing"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "pre_provisioning_hook.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "provisioning_role_arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "template_body"), + ), + }, + }, + }) +} + +func testAccCheckProvisioningTemplateExists(n string) 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 Provisioning Template ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).IoTConn + + _, err := tfiot.FindProvisioningTemplateByName(context.TODO(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckProvisioningTemplateDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IoTConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iot_provisioning_template" { + continue + } + + _, err := tfiot.FindProvisioningTemplateByName(context.TODO(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("IoT Provisioning Template %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckProvisioningTemplateNumVersions(name string, want int) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IoTConn + + var got int + err := conn.ListProvisioningTemplateVersionsPages( + &iot.ListProvisioningTemplateVersionsInput{TemplateName: aws.String(name)}, + func(page *iot.ListProvisioningTemplateVersionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + got += len(page.Versions) + + return !lastPage + }) + + if err != nil { + return err + } + + if got != want { + return fmt.Errorf("Incorrect version count for IoT Provisioning Template %s; got: %d, want: %d", name, got, want) + } + + return nil + } +} + +func testAccProvisioningTemplateBaseConfig(rName string) string { + return fmt.Sprintf(` +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["iot.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/service-role/" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_partition" "current" {} + +resource "aws_iam_role_policy_attachment" "test" { + role = aws_iam_role.test.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSIoTThingsRegistration" +} + +data "aws_iam_policy_document" "device" { + statement { + actions = ["iot:Subscribe"] + resources = ["*"] + } +} + +resource "aws_iot_policy" "test" { + name = %[1]q + policy = data.aws_iam_policy_document.device.json +} +`, rName) +} + +func testAccProvisioningTemplateConfig(rName string) string { + return acctest.ConfigCompose(testAccProvisioningTemplateBaseConfig(rName), fmt.Sprintf(` +resource "aws_iot_provisioning_template" "test" { + name = %[1]q + provisioning_role_arn = aws_iam_role.test.arn + + template_body = jsonencode({ + Parameters = { + SerialNumber = { Type = "String" } + } + + Resources = { + certificate = { + Properties = { + CertificateId = { Ref = "AWS::IoT::Certificate::Id" } + Status = "Active" + } + Type = "AWS::IoT::Certificate" + } + + policy = { + Properties = { + PolicyName = aws_iot_policy.test.name + } + Type = "AWS::IoT::Policy" + } + } + }) +} +`, rName)) +} + +func testAccProvisioningTemplateConfigTags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccProvisioningTemplateBaseConfig(rName), fmt.Sprintf(` +resource "aws_iot_provisioning_template" "test" { + name = %[1]q + provisioning_role_arn = aws_iam_role.test.arn + + template_body = jsonencode({ + Parameters = { + SerialNumber = { Type = "String" } + } + + Resources = { + certificate = { + Properties = { + CertificateId = { Ref = "AWS::IoT::Certificate::Id" } + Status = "Active" + } + Type = "AWS::IoT::Certificate" + } + + policy = { + Properties = { + PolicyName = aws_iot_policy.test.name + } + Type = "AWS::IoT::Policy" + } + } + }) + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccProvisioningTemplateConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccProvisioningTemplateBaseConfig(rName), fmt.Sprintf(` +resource "aws_iot_provisioning_template" "test" { + name = %[1]q + provisioning_role_arn = aws_iam_role.test.arn + + template_body = jsonencode({ + Parameters = { + SerialNumber = { Type = "String" } + } + + Resources = { + certificate = { + Properties = { + CertificateId = { Ref = "AWS::IoT::Certificate::Id" } + Status = "Active" + } + Type = "AWS::IoT::Certificate" + } + + policy = { + Properties = { + PolicyName = aws_iot_policy.test.name + } + Type = "AWS::IoT::Policy" + } + } + }) + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} + +func testAccProvisioningTemplateUpdatedConfig(rName string) string { + return acctest.ConfigCompose(testAccProvisioningTemplateBaseConfig(rName), fmt.Sprintf(` +resource "aws_iot_provisioning_template" "test" { + name = %[1]q + provisioning_role_arn = aws_iam_role.test.arn + description = "For testing" + enabled = true + + template_body = jsonencode({ + Parameters = { + SerialNumber = { Type = "String" } + } + + Resources = { + certificate = { + Properties = { + CertificateId = { Ref = "AWS::IoT::Certificate::Id" } + Status = "Inactive" + } + Type = "AWS::IoT::Certificate" + } + + policy = { + Properties = { + PolicyName = aws_iot_policy.test.name + } + Type = "AWS::IoT::Policy" + } + } + }) +} +`, rName)) +} diff --git a/internal/tfresource/retry.go b/internal/tfresource/retry.go index 9042a3ce128..eb1c5119454 100644 --- a/internal/tfresource/retry.go +++ b/internal/tfresource/retry.go @@ -73,6 +73,22 @@ func RetryWhenAWSErrCodeEquals(timeout time.Duration, f func() (interface{}, err return RetryWhenAWSErrCodeEqualsContext(context.Background(), timeout, f, codes...) } +// RetryWhenAWSErrMessageContainsContext retries the specified function when it returns an AWS error containing the specified message. +func RetryWhenAWSErrMessageContainsContext(ctx context.Context, timeout time.Duration, f func() (interface{}, error), code, message string) (interface{}, error) { + return RetryWhenContext(ctx, timeout, f, func(err error) (bool, error) { + if tfawserr.ErrMessageContains(err, code, message) { + return true, err + } + + return false, err + }) +} + +// RetryWhenAWSErrMessageContains retries the specified function when it returns an AWS error containing the specified message. +func RetryWhenAWSErrMessageContains(timeout time.Duration, f func() (interface{}, error), code, message string) (interface{}, error) { + return RetryWhenAWSErrMessageContainsContext(context.Background(), timeout, f, code, message) +} + var resourceFoundError = errors.New(`found resource`) // RetryUntilNotFoundContext retries the specified function until it returns a resource.NotFoundError. diff --git a/internal/tfresource/retry_test.go b/internal/tfresource/retry_test.go index eb8d7049e36..8eb11c1c8b3 100644 --- a/internal/tfresource/retry_test.go +++ b/internal/tfresource/retry_test.go @@ -75,6 +75,68 @@ func TestRetryWhenAWSErrCodeEquals(t *testing.T) { } } +func TestRetryWhenAWSErrMessageContains(t *testing.T) { + var retryCount int32 + + testCases := []struct { + Name string + F func() (interface{}, error) + ExpectError bool + }{ + { + Name: "no error", + F: func() (interface{}, error) { + return nil, nil + }, + }, + { + Name: "non-retryable other error", + F: func() (interface{}, error) { + return nil, errors.New("TestCode") + }, + ExpectError: true, + }, + { + Name: "non-retryable AWS error", + F: func() (interface{}, error) { + return nil, awserr.New("TestCode1", "Testing", nil) + }, + ExpectError: true, + }, + { + Name: "retryable AWS error timeout", + F: func() (interface{}, error) { + return nil, awserr.New("TestCode1", "TestMessage1", nil) + }, + ExpectError: true, + }, + { + Name: "retryable AWS error success", + F: func() (interface{}, error) { + if atomic.CompareAndSwapInt32(&retryCount, 0, 1) { + return nil, awserr.New("TestCode1", "TestMessage1", nil) + } + + return nil, nil + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + retryCount = 0 + + _, err := tfresource.RetryWhenAWSErrMessageContains(5*time.Second, testCase.F, "TestCode1", "TestMessage1") + + if testCase.ExpectError && err == nil { + t.Fatal("expected error") + } else if !testCase.ExpectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + }) + } +} + func TestRetryWhenNewResourceNotFound(t *testing.T) { var retryCount int32 diff --git a/website/docs/r/iot_provisioning_template.html.markdown b/website/docs/r/iot_provisioning_template.html.markdown new file mode 100644 index 00000000000..8ed4db80fac --- /dev/null +++ b/website/docs/r/iot_provisioning_template.html.markdown @@ -0,0 +1,113 @@ +--- +subcategory: "IoT" +layout: "aws" +page_title: "AWS: aws_iot_provisioning_template" +description: |- + Manages an IoT fleet provisioning template. +--- + +# Resource: aws_iot_provisioning_template + +Manages an IoT fleet provisioning template. For more info, see the AWS documentation on [fleet provisioning](https://docs.aws.amazon.com/iot/latest/developerguide/provision-wo-cert.html). + +## Example Usage + +```terraform +data "aws_iam_policy_document" "iot_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["iot.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "iot_fleet_provisioning" { + name = "IoTProvisioningServiceRole" + path = "/service-role/" + assume_role_policy = data.aws_iam_policy_document.iot_assume_role_policy.json +} + +resource "aws_iam_role_policy_attachment" "iot_fleet_provisioning_registration" { + role = aws_iam_role.iot_fleet_provisioning.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration" +} + +data "aws_iam_policy_document" "device_policy" { + statement { + actions = ["iot:Subscribe"] + resources = ["*"] + } +} + +resource "aws_iot_policy" "device_policy" { + name = "DevicePolicy" + policy = data.aws_iam_policy_document.device_policy.json +} + +resource "aws_iot_provisioning_template" "fleet" { + name = "FleetTemplate" + description = "My provisioning template" + provisioning_role_arn = aws_iam_role.iot_fleet_provisioning.arn + + template_body = jsonencode({ + Parameters = { + SerialNumber = { Type = "String" } + } + + Resources = { + certificate = { + Properties = { + CertificateId = { Ref = "AWS::IoT::Certificate::Id" } + Status = "Active" + } + Type = "AWS::IoT::Certificate" + } + + policy = { + Properties = { + PolicyName = aws_iot_policy.device_policy.name + } + Type = "AWS::IoT::Policy" + } + } + }) +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the fleet provisioning template. +* `description` - (Optional) The description of the fleet provisioning template. +* `enabled` - (Optional) True to enable the fleet provisioning template, otherwise false. +* `pre_provisioning_hook` - (Optional) Creates a pre-provisioning hook template. Details below. +* `provisioning_role_arn` - (Required) The role ARN for the role associated with the fleet provisioning template. This IoT role grants permission to provision a device. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `template_body` - (Required) The JSON formatted contents of the fleet provisioning template. + +### pre_provisioning_hook + +The `pre_provisioning_hook` configuration block supports the following: + +* `payload_version` - (Optional) The version of the payload that was sent to the target function. The only valid (and the default) payload version is `"2020-04-01"`. +* `target_arb` - (Optional) The ARN of the target function. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The ARN that identifies the provisioning template. +* `default_version_id` - The default version of the fleet provisioning template. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +IoT fleet provisioning templates can be imported using the `name`, e.g. + +``` +$ terraform import aws_iot_provisioning_template.fleet FleetProvisioningTemplate +``` \ No newline at end of file