Skip to content

Commit c1eee85

Browse files
authored
Merge pull request #43916 from tabito-hara/f-aws_iot_thing_principal_attachment-support_thing_principal_type
[Enhancement] aws_iot_thing_principal_attachment: Support `thing_principal_type` argument
2 parents f87c351 + 066be08 commit c1eee85

File tree

4 files changed

+204
-11
lines changed

4 files changed

+204
-11
lines changed

.changelog/43916.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_iot_thing_principal_attachment: Add `thing_principal_type` argument
3+
```

internal/service/iot/thing_principal_attachment.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"log"
10+
"strings"
1011

1112
"github.com/aws/aws-sdk-go-v2/aws"
1213
"github.com/aws/aws-sdk-go-v2/service/iot"
@@ -15,6 +16,7 @@ import (
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1617
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1718
"github.com/hashicorp/terraform-provider-aws/internal/conns"
19+
"github.com/hashicorp/terraform-provider-aws/internal/enum"
1820
"github.com/hashicorp/terraform-provider-aws/internal/errs"
1921
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
2022
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
@@ -29,6 +31,10 @@ func resourceThingPrincipalAttachment() *schema.Resource {
2931
ReadWithoutTimeout: resourceThingPrincipalAttachmentRead,
3032
DeleteWithoutTimeout: resourceThingPrincipalAttachmentDelete,
3133

34+
Importer: &schema.ResourceImporter{
35+
StateContext: schema.ImportStatePassthroughContext,
36+
},
37+
3238
Schema: map[string]*schema.Schema{
3339
names.AttrPrincipal: {
3440
Type: schema.TypeString,
@@ -40,6 +46,13 @@ func resourceThingPrincipalAttachment() *schema.Resource {
4046
Required: true,
4147
ForceNew: true,
4248
},
49+
"thing_principal_type": {
50+
Type: schema.TypeString,
51+
Optional: true,
52+
Computed: true,
53+
ForceNew: true,
54+
ValidateDiagFunc: enum.Validate[awstypes.ThingPrincipalType](),
55+
},
4356
},
4457
}
4558
}
@@ -56,6 +69,10 @@ func resourceThingPrincipalAttachmentCreate(ctx context.Context, d *schema.Resou
5669
ThingName: aws.String(thing),
5770
}
5871

72+
if v, ok := d.Get("thing_principal_type").(string); ok {
73+
input.ThingPrincipalType = awstypes.ThingPrincipalType(v)
74+
}
75+
5976
_, err := conn.AttachThingPrincipal(ctx, input)
6077

6178
if err != nil {
@@ -71,10 +88,15 @@ func resourceThingPrincipalAttachmentRead(ctx context.Context, d *schema.Resourc
7188
var diags diag.Diagnostics
7289
conn := meta.(*conns.AWSClient).IoTClient(ctx)
7390

74-
principal := d.Get(names.AttrPrincipal).(string)
75-
thing := d.Get("thing").(string)
91+
id := d.Id()
92+
parts := strings.Split(id, "|")
93+
if len(parts) != 2 {
94+
return sdkdiag.AppendErrorf(diags, "unexpected format for ID (%s), expected thing|principal", id)
95+
}
96+
thing := parts[0]
97+
principal := parts[1]
7698

77-
_, err := findThingPrincipalAttachmentByTwoPartKey(ctx, conn, thing, principal)
99+
out, err := findThingPrincipalAttachmentByTwoPartKey(ctx, conn, thing, principal)
78100

79101
if !d.IsNewResource() && tfresource.NotFound(err) {
80102
log.Printf("[WARN] IoT Thing Principal Attachment (%s) not found, removing from state", d.Id())
@@ -86,6 +108,10 @@ func resourceThingPrincipalAttachmentRead(ctx context.Context, d *schema.Resourc
86108
return sdkdiag.AppendErrorf(diags, "reading IoT Thing Principal Attachment (%s): %s", d.Id(), err)
87109
}
88110

111+
d.Set(names.AttrPrincipal, out.Principal)
112+
d.Set("thing", thing)
113+
d.Set("thing_principal_type", out.ThingPrincipalType)
114+
89115
return diags
90116
}
91117

@@ -110,8 +136,8 @@ func resourceThingPrincipalAttachmentDelete(ctx context.Context, d *schema.Resou
110136
return diags
111137
}
112138

113-
func findThingPrincipalAttachmentByTwoPartKey(ctx context.Context, conn *iot.Client, thing, principal string) (*string, error) {
114-
input := &iot.ListThingPrincipalsInput{
139+
func findThingPrincipalAttachmentByTwoPartKey(ctx context.Context, conn *iot.Client, thing, principal string) (*awstypes.ThingPrincipalObject, error) {
140+
input := &iot.ListThingPrincipalsV2Input{
115141
ThingName: aws.String(thing),
116142
}
117143

@@ -120,7 +146,7 @@ func findThingPrincipalAttachmentByTwoPartKey(ctx context.Context, conn *iot.Cli
120146
})
121147
}
122148

123-
func findThingPrincipal(ctx context.Context, conn *iot.Client, input *iot.ListThingPrincipalsInput, filter tfslices.Predicate[string]) (*string, error) {
149+
func findThingPrincipal(ctx context.Context, conn *iot.Client, input *iot.ListThingPrincipalsV2Input, filter tfslices.Predicate[string]) (*awstypes.ThingPrincipalObject, error) {
124150
output, err := findThingPrincipals(ctx, conn, input, filter)
125151

126152
if err != nil {
@@ -130,10 +156,10 @@ func findThingPrincipal(ctx context.Context, conn *iot.Client, input *iot.ListTh
130156
return tfresource.AssertSingleValueResult(output)
131157
}
132158

133-
func findThingPrincipals(ctx context.Context, conn *iot.Client, input *iot.ListThingPrincipalsInput, filter tfslices.Predicate[string]) ([]string, error) {
134-
var output []string
159+
func findThingPrincipals(ctx context.Context, conn *iot.Client, input *iot.ListThingPrincipalsV2Input, filter tfslices.Predicate[string]) ([]awstypes.ThingPrincipalObject, error) {
160+
var output []awstypes.ThingPrincipalObject
135161

136-
pages := iot.NewListThingPrincipalsPaginator(conn, input)
162+
pages := iot.NewListThingPrincipalsV2Paginator(conn, input)
137163
for pages.HasMorePages() {
138164
page, err := pages.NextPage(ctx)
139165

@@ -148,8 +174,8 @@ func findThingPrincipals(ctx context.Context, conn *iot.Client, input *iot.ListT
148174
return nil, err
149175
}
150176

151-
for _, v := range page.Principals {
152-
if filter(v) {
177+
for _, v := range page.ThingPrincipalObjects {
178+
if filter(aws.ToString(v.Principal)) {
153179
output = append(output, v)
154180
}
155181
}

internal/service/iot/thing_principal_attachment_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"testing"
1010

11+
"github.com/YakDriver/regexache"
1112
"github.com/aws/aws-sdk-go-v2/aws"
1213
"github.com/aws/aws-sdk-go-v2/service/iot"
1314
awstypes "github.com/aws/aws-sdk-go-v2/service/iot/types"
@@ -26,6 +27,7 @@ func TestAccIoTThingPrincipalAttachment_basic(t *testing.T) {
2627
ctx := acctest.Context(t)
2728
thingName := sdkacctest.RandomWithPrefix("tf-acc")
2829
thingName2 := sdkacctest.RandomWithPrefix("tf-acc2")
30+
resourceName := "aws_iot_thing_principal_attachment.att"
2931

3032
resource.ParallelTest(t, resource.TestCase{
3133
PreCheck: func() { acctest.PreCheck(ctx, t) },
@@ -38,8 +40,14 @@ func TestAccIoTThingPrincipalAttachment_basic(t *testing.T) {
3840
Check: resource.ComposeTestCheckFunc(
3941
testAccCheckThingPrincipalAttachmentExists(ctx, "aws_iot_thing_principal_attachment.att"),
4042
testAccCheckThingPrincipalAttachmentStatus(ctx, thingName, true, []string{"aws_iot_certificate.cert"}),
43+
resource.TestCheckResourceAttr(resourceName, "thing_principal_type", string(awstypes.ThingPrincipalTypeNonExclusiveThing)),
4144
),
4245
},
46+
{
47+
ResourceName: resourceName,
48+
ImportState: true,
49+
ImportStateVerify: true,
50+
},
4351
{
4452
Config: testAccThingPrincipalAttachmentConfig_update1(thingName, thingName2),
4553
Check: resource.ComposeTestCheckFunc(
@@ -76,6 +84,63 @@ func TestAccIoTThingPrincipalAttachment_basic(t *testing.T) {
7684
},
7785
})
7886
}
87+
func TestAccIoTThingPrincipalAttachment_thingPrincipalType(t *testing.T) {
88+
ctx := acctest.Context(t)
89+
thingName := sdkacctest.RandomWithPrefix("tf-acc")
90+
thingName2 := sdkacctest.RandomWithPrefix("tf-acc2")
91+
resourceName := "aws_iot_thing_principal_attachment.att"
92+
resourceThingName := "aws_iot_thing.thing"
93+
resourceCertName := "aws_iot_certificate.cert"
94+
95+
resource.ParallelTest(t, resource.TestCase{
96+
PreCheck: func() { acctest.PreCheck(ctx, t) },
97+
ErrorCheck: acctest.ErrorCheck(t, names.IoTServiceID),
98+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
99+
CheckDestroy: testAccCheckThingPrincipalAttachmentDestroy(ctx),
100+
Steps: []resource.TestStep{
101+
{
102+
Config: testAccThingPrincipalAttachmentConfig_thingPrincipalType(thingName),
103+
Check: resource.ComposeTestCheckFunc(
104+
testAccCheckThingPrincipalAttachmentExists(ctx, "aws_iot_thing_principal_attachment.att"),
105+
testAccCheckThingPrincipalAttachmentStatus(ctx, thingName, true, []string{"aws_iot_certificate.cert"}),
106+
resource.TestCheckResourceAttr(resourceName, "thing_principal_type", string(awstypes.ThingPrincipalTypeExclusiveThing)),
107+
resource.TestCheckResourceAttrPair(resourceName, "thing", resourceThingName, names.AttrName),
108+
resource.TestCheckResourceAttrPair(resourceName, names.AttrPrincipal, resourceCertName, names.AttrARN),
109+
),
110+
},
111+
{
112+
ResourceName: resourceName,
113+
ImportState: true,
114+
ImportStateVerify: true,
115+
},
116+
{
117+
// The first attachment is specified as EXCLUSIVE_THING.
118+
// Try to attach the same principal to another Thing, which should fail
119+
// because exclusive principals can only be attached to one thing.
120+
Config: testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate1(thingName, thingName2),
121+
ExpectError: regexache.MustCompile(`InvalidRequestException: Principal already has an exclusive Thing attached to it`),
122+
},
123+
{
124+
// Reset to one attachment with NON_EXCLUSIVE_THING.
125+
Config: testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate2(thingName),
126+
Check: resource.ComposeTestCheckFunc(
127+
testAccCheckThingPrincipalAttachmentExists(ctx, "aws_iot_thing_principal_attachment.att"),
128+
testAccCheckThingPrincipalAttachmentStatus(ctx, thingName, true, []string{"aws_iot_certificate.cert"}),
129+
resource.TestCheckResourceAttr(resourceName, "thing_principal_type", string(awstypes.ThingPrincipalTypeNonExclusiveThing)),
130+
resource.TestCheckResourceAttrPair(resourceName, "thing", resourceThingName, names.AttrName),
131+
resource.TestCheckResourceAttrPair(resourceName, names.AttrPrincipal, resourceCertName, names.AttrARN),
132+
),
133+
},
134+
{
135+
// Try to attach the same principal to another Thing specifying EXCLUSIVE_THING,
136+
// which should fail because the principal already has a non-exclusive attachment
137+
// and exclusive attachments cannot coexist with any other attachments.
138+
Config: testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate3(thingName, thingName2),
139+
ExpectError: regexache.MustCompile(`InvalidRequestException: Principal already has a Thing attached to it`),
140+
},
141+
},
142+
})
143+
}
79144

80145
func testAccCheckThingPrincipalAttachmentDestroy(ctx context.Context) resource.TestCheckFunc {
81146
return func(s *terraform.State) error {
@@ -282,3 +347,101 @@ resource "aws_iot_thing_principal_attachment" "att2" {
282347
}
283348
`, thingName)
284349
}
350+
351+
func testAccThingPrincipalAttachmentConfig_thingPrincipalType(thingName string) string {
352+
return fmt.Sprintf(`
353+
resource "aws_iot_certificate" "cert" {
354+
csr = file("test-fixtures/iot-csr.pem")
355+
active = true
356+
}
357+
358+
resource "aws_iot_thing" "thing" {
359+
name = "%s"
360+
}
361+
362+
resource "aws_iot_thing_principal_attachment" "att" {
363+
thing = aws_iot_thing.thing.name
364+
principal = aws_iot_certificate.cert.arn
365+
366+
thing_principal_type = "EXCLUSIVE_THING"
367+
}
368+
`, thingName)
369+
}
370+
371+
func testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate1(thingName, thingName2 string) string {
372+
return fmt.Sprintf(`
373+
resource "aws_iot_certificate" "cert" {
374+
csr = file("test-fixtures/iot-csr.pem")
375+
active = true
376+
}
377+
378+
resource "aws_iot_thing" "thing" {
379+
name = %[1]q
380+
}
381+
382+
resource "aws_iot_thing" "thing2" {
383+
name = %[2]q
384+
}
385+
386+
resource "aws_iot_thing_principal_attachment" "att" {
387+
thing = aws_iot_thing.thing.name
388+
principal = aws_iot_certificate.cert.arn
389+
390+
thing_principal_type = "EXCLUSIVE_THING"
391+
}
392+
393+
resource "aws_iot_thing_principal_attachment" "att2" {
394+
thing = aws_iot_thing.thing2.name
395+
principal = aws_iot_certificate.cert.arn
396+
}
397+
`, thingName, thingName2)
398+
}
399+
400+
func testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate2(thingName string) string {
401+
return fmt.Sprintf(`
402+
resource "aws_iot_certificate" "cert" {
403+
csr = file("test-fixtures/iot-csr.pem")
404+
active = true
405+
}
406+
407+
resource "aws_iot_thing" "thing" {
408+
name = "%s"
409+
}
410+
411+
resource "aws_iot_thing_principal_attachment" "att" {
412+
thing = aws_iot_thing.thing.name
413+
principal = aws_iot_certificate.cert.arn
414+
415+
thing_principal_type = "NON_EXCLUSIVE_THING"
416+
}
417+
`, thingName)
418+
}
419+
420+
func testAccThingPrincipalAttachmentConfig_thingPrincipalTypeUpdate3(thingName, thingName2 string) string {
421+
return fmt.Sprintf(`
422+
resource "aws_iot_certificate" "cert" {
423+
csr = file("test-fixtures/iot-csr.pem")
424+
active = true
425+
}
426+
427+
resource "aws_iot_thing" "thing" {
428+
name = %[1]q
429+
}
430+
431+
resource "aws_iot_thing" "thing2" {
432+
name = %[2]q
433+
}
434+
435+
resource "aws_iot_thing_principal_attachment" "att" {
436+
thing = aws_iot_thing.thing.name
437+
principal = aws_iot_certificate.cert.arn
438+
}
439+
440+
resource "aws_iot_thing_principal_attachment" "att2" {
441+
thing = aws_iot_thing.thing2.name
442+
principal = aws_iot_certificate.cert.arn
443+
444+
thing_principal_type = "EXCLUSIVE_THING"
445+
}
446+
`, thingName, thingName2)
447+
}

website/docs/r/iot_thing_principal_attachment.html.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ This resource supports the following arguments:
3535
* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference).
3636
* `principal` - (Required) The AWS IoT Certificate ARN or Amazon Cognito Identity ID.
3737
* `thing` - (Required) The name of the thing.
38+
* `thing_principal_type` - (Optional) The type of relationship to specify when attaching a principal to a thing. Valid values are `EXCLUSIVE_THING` (the thing will be the only one attached to the principal) or `NON_EXCLUSIVE_THING` (multiple things can be attached to the principal). Defaults to `NON_EXCLUSIVE_THING`.
3839

3940
## Attribute Reference
4041

0 commit comments

Comments
 (0)