From 7ef1151caab889f3cbc2ee947ae9ecd9c6bbf40f Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Fri, 29 Aug 2025 08:55:13 -0400 Subject: [PATCH] Add support for importing aws_dynamodb_table_item --- .changelog/44089.txt | 3 + internal/service/dynamodb/table_item.go | 100 ++++++++++++++++++ internal/service/dynamodb/table_item_test.go | 32 +++++- .../docs/r/dynamodb_table_item.html.markdown | 15 ++- 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 .changelog/44089.txt diff --git a/.changelog/44089.txt b/.changelog/44089.txt new file mode 100644 index 000000000000..4df94e4b1339 --- /dev/null +++ b/.changelog/44089.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dynamodb_table_item: Add import support +``` \ No newline at end of file diff --git a/internal/service/dynamodb/table_item.go b/internal/service/dynamodb/table_item.go index dc6fbe90c2ff..5030bbbab617 100644 --- a/internal/service/dynamodb/table_item.go +++ b/internal/service/dynamodb/table_item.go @@ -33,6 +33,10 @@ func resourceTableItem() *schema.Resource { UpdateWithoutTimeout: resourceTableItemUpdate, DeleteWithoutTimeout: resourceTableItemDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceTableItemImportState, + }, + Schema: map[string]*schema.Schema{ "hash_key": { Type: schema.TypeString, @@ -309,3 +313,99 @@ func expandTableItemQueryKey(attrs map[string]awstypes.AttributeValue, hashKey, return queryKey } + +func createTableItemKeyAttr(attrTypes map[string]awstypes.ScalarAttributeType, name, value string) (awstypes.AttributeValue, error) { + attrType, ok := attrTypes[name] + if !ok { + return nil, fmt.Errorf("key %s not found in attribute definitions", name) + } + switch attrType { + case awstypes.ScalarAttributeTypeS: + return &awstypes.AttributeValueMemberS{Value: value}, nil + case awstypes.ScalarAttributeTypeN: + return &awstypes.AttributeValueMemberN{Value: value}, nil + case awstypes.ScalarAttributeTypeB: + data, err := itypes.Base64Decode(value) + if err != nil { + return nil, fmt.Errorf("invalid base64 value for binary attribute %s: %s", name, err) + } + return &awstypes.AttributeValueMemberB{Value: data}, nil + default: + return nil, fmt.Errorf("unsupported attribute type: %s", attrType) + } +} + +func resourceTableItemImportState(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + conn := meta.(*conns.AWSClient).DynamoDBClient(ctx) + + idParts := strings.Split(d.Id(), "|") + if len(idParts) < 3 || len(idParts) > 4 { + return nil, fmt.Errorf("unexpected format for import ID (%s), expected tableName|hashKeyName|hashKeyValue[|rangeKeyValue]", d.Id()) + } + + tableName := idParts[0] + hashKey := idParts[1] + hashValue := idParts[2] + var rangeValue string + if len(idParts) == 4 { + rangeValue = idParts[3] + } + + output, err := conn.DescribeTable(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err != nil { + return nil, fmt.Errorf("describing table %s: %s", tableName, err) + } + + var rangeKey string + if rangeValue != "" { + var found bool + for _, elem := range output.Table.KeySchema { + if aws.ToString(elem.AttributeName) != hashKey && elem.KeyType == awstypes.KeyTypeRange { + rangeKey = aws.ToString(elem.AttributeName) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("import ID contains range key value but table %s does not have a range key", tableName) + } + } + + attrTypes := map[string]awstypes.ScalarAttributeType{} + for _, v := range output.Table.AttributeDefinitions { + attrTypes[aws.ToString(v.AttributeName)] = v.AttributeType + } + + key := map[string]awstypes.AttributeValue{} + key[hashKey], err = createTableItemKeyAttr(attrTypes, hashKey, hashValue) + if err != nil { + return nil, err + } + if rangeValue != "" { + key[rangeKey], err = createTableItemKeyAttr(attrTypes, rangeKey, rangeValue) + if err != nil { + return nil, err + } + } + + item, err := findTableItemByTwoPartKey(ctx, conn, tableName, key) + if err != nil { + return nil, fmt.Errorf("reading DynamoDB Table Item: %s: %s", d.Id(), err) + } + itemAttrs, err := flattenTableItemAttributes(item) + if err != nil { + return nil, fmt.Errorf("flattening item attributes: %s", err) + } + + d.Set(names.AttrTableName, tableName) + d.Set("hash_key", hashKey) + if rangeKey != "" { + d.Set("range_key", rangeKey) + } + d.Set("item", itemAttrs) + d.SetId(tableItemCreateResourceID(tableName, hashKey, rangeKey, item)) + + return []*schema.ResourceData{d}, nil +} diff --git a/internal/service/dynamodb/table_item_test.go b/internal/service/dynamodb/table_item_test.go index 4e0d23a2574e..00520c4ec24e 100644 --- a/internal/service/dynamodb/table_item_test.go +++ b/internal/service/dynamodb/table_item_test.go @@ -5,6 +5,7 @@ package dynamodb_test import ( "context" + "encoding/json" "fmt" "testing" @@ -28,13 +29,13 @@ func TestAccDynamoDBTableItem_basic(t *testing.T) { tableName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) hashKey := "hashKey" - itemContent := `{ + itemContent := testAccNormalizeItemJSON(t, `{ "hashKey": {"S": "something"}, "one": {"N": "11111"}, "two": {"N": "22222"}, "three": {"N": "33333"}, "four": {"N": "44444"} -}` +}`) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -52,6 +53,11 @@ func TestAccDynamoDBTableItem_basic(t *testing.T) { acctest.CheckResourceAttrEquivalentJSON("aws_dynamodb_table_item.test", "item", itemContent), ), }, + { + ResourceName: "aws_dynamodb_table_item.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -63,14 +69,14 @@ func TestAccDynamoDBTableItem_rangeKey(t *testing.T) { tableName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) hashKey := "hashKey" rangeKey := "rangeKey" - itemContent := `{ + itemContent := testAccNormalizeItemJSON(t, `{ "hashKey": {"S": "something"}, "rangeKey": {"S": "something-else"}, "one": {"N": "11111"}, "two": {"N": "22222"}, "three": {"N": "33333"}, "four": {"N": "44444"} -}` +}`) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -89,6 +95,11 @@ func TestAccDynamoDBTableItem_rangeKey(t *testing.T) { acctest.CheckResourceAttrEquivalentJSON("aws_dynamodb_table_item.test", "item", itemContent), ), }, + { + ResourceName: "aws_dynamodb_table_item.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -717,3 +728,16 @@ ITEM } `, tableName, hashKey, content) } + +func testAccNormalizeItemJSON(t *testing.T, item string) string { + t.Helper() + var data any + if err := json.Unmarshal([]byte(item), &data); err != nil { + t.Fatalf("failed to unmarshal JSON: %s", err) + } + normalized, err := json.Marshal(data) + if err != nil { + t.Fatalf("failed to marshal JSON: %s", err) + } + return string(normalized) +} diff --git a/website/docs/r/dynamodb_table_item.html.markdown b/website/docs/r/dynamodb_table_item.html.markdown index 8e8af3ecccf4..b38e981824d8 100644 --- a/website/docs/r/dynamodb_table_item.html.markdown +++ b/website/docs/r/dynamodb_table_item.html.markdown @@ -62,4 +62,17 @@ This resource exports the following attributes in addition to the arguments abov ## Import -You cannot import DynamoDB table items. +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DynamoDB table items using the `table_name|hash_key_name|hash_key_value[|range_key_value]`. For example: + +```terraform +import { + to = aws_dynamodb_table_item.example + id = "example-table|exampleHashKey|exampleHashValue" +} +``` + +Using `terraform import`, import DynamoDB table items using the `table_name|hash_key_name|hash_key_value[|range_key_value]`. For example: + +```console +% terraform import aws_dynamodb_table_item.example 'example-table|exampleHashKey|exampleHashValue' +```