Skip to content

Commit 767d66f

Browse files
committed
feat(mnq/sqs): support for dead_letter_queue
1 parent 9e6eb04 commit 767d66f

File tree

7 files changed

+3485
-6753
lines changed

7 files changed

+3485
-6753
lines changed

internal/services/mnq/helpers_mnq.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mnq
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"strconv"
@@ -172,11 +173,9 @@ func ComposeSNSARN(region scw.Region, projectID string, resourceName string) str
172173
return composeARN("sns", region, projectID, resourceName)
173174
}
174175

175-
// Set the value inside values at the resource path (e.g. a.0.b sets b's value)
176176
func setResourceValue(values map[string]any, resourcePath string, value any, resourceSchemas map[string]*schema.Schema) {
177177
parts := strings.Split(resourcePath, ".")
178178
if len(parts) > 1 {
179-
// Terraform's nested objects are represented as slices of maps
180179
if _, ok := values[parts[0]]; !ok {
181180
values[parts[0]] = []any{make(map[string]any)}
182181
}
@@ -224,6 +223,60 @@ func awsResourceDataToAttribute(awsAttributes map[string]string, awsAttribute st
224223
s = strconv.Itoa(resourceValue.(int))
225224
case schema.TypeString:
226225
s = resourceValue.(string)
226+
case schema.TypeList:
227+
// Handle dead-letter queue configuration
228+
if resourcePath == "dead_letter_queue" {
229+
deadLetterConfig := resourceValue.([]any)
230+
if len(deadLetterConfig) > 0 {
231+
config := deadLetterConfig[0].(map[string]any)
232+
queueID := config["id"].(string)
233+
maxReceiveCount := config["max_receive_count"].(int)
234+
235+
// Build AWS-compatible ARN for SQS
236+
// The queueID can be either:
237+
// 1. A Terraform ID in format: region/project-id/queue-name
238+
// 2. A Scaleway ARN in format: arn:scw:sqs:region:project-id:queue-name
239+
240+
var scwARN string
241+
242+
switch {
243+
case strings.HasPrefix(queueID, "arn:scw:sqs:"):
244+
// Already a Scaleway ARN, use it directly
245+
scwARN = queueID
246+
case strings.Contains(queueID, "/"):
247+
// Parse Terraform ID format: region/project-id/queue-name
248+
parts := strings.Split(queueID, "/")
249+
if len(parts) == 3 {
250+
region := parts[0]
251+
projectID := parts[1]
252+
queueName := parts[2]
253+
254+
// Build Scaleway ARN: arn:scw:sqs:region:project-id:queue-name
255+
scwARN = fmt.Sprintf("arn:scw:sqs:%s:project-%s:%s", region, projectID, queueName)
256+
} else {
257+
return fmt.Errorf("invalid queue ID format for dead-letter queue: %s (expected region/project-id/queue-name or arn:scw:sqs:region:project-id:queue-name)", queueID)
258+
}
259+
default:
260+
// Assume it's already a valid ARN or queue identifier
261+
scwARN = queueID
262+
}
263+
264+
// Create RedrivePolicy JSON
265+
redrivePolicy := map[string]any{
266+
"deadLetterTargetArn": scwARN,
267+
"maxReceiveCount": maxReceiveCount,
268+
}
269+
270+
jsonData, err := json.Marshal(redrivePolicy)
271+
if err != nil {
272+
return fmt.Errorf("failed to marshal redrive policy: %w", err)
273+
}
274+
275+
s = string(jsonData)
276+
}
277+
} else {
278+
return fmt.Errorf("unsupported list type for %s", resourcePath)
279+
}
227280
default:
228281
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
229282
}
@@ -265,14 +318,47 @@ func awsAttributeToResourceData(values map[string]any, value string, resourcePat
265318
setResourceValue(values, resourcePath, i, resourceSchemas)
266319
case schema.TypeString:
267320
setResourceValue(values, resourcePath, value, resourceSchemas)
321+
case schema.TypeList:
322+
if resourcePath == "dead_letter_queue" && value != "" {
323+
var redrivePolicy map[string]any
324+
if err := json.Unmarshal([]byte(value), &redrivePolicy); err != nil {
325+
return fmt.Errorf("failed to unmarshal redrive policy: %w", err)
326+
}
327+
328+
deadLetterTargetArn := redrivePolicy["deadLetterTargetArn"].(string)
329+
330+
var terraformID string
331+
332+
if strings.HasPrefix(deadLetterTargetArn, "arn:scw:sqs:") {
333+
parts := strings.Split(deadLetterTargetArn, ":")
334+
if len(parts) >= 6 {
335+
region := parts[3]
336+
projectID := strings.TrimPrefix(parts[4], "project-")
337+
queueName := parts[5]
338+
terraformID = fmt.Sprintf("%s/%s/%s", region, projectID, queueName)
339+
} else {
340+
terraformID = deadLetterTargetArn
341+
}
342+
} else {
343+
terraformID = deadLetterTargetArn
344+
}
345+
346+
deadLetterConfig := map[string]any{
347+
"id": terraformID,
348+
"max_receive_count": int(redrivePolicy["maxReceiveCount"].(float64)),
349+
}
350+
351+
setResourceValue(values, resourcePath, []any{deadLetterConfig}, resourceSchemas)
352+
} else {
353+
return fmt.Errorf("unsupported list type for %s", resourcePath)
354+
}
268355
default:
269356
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
270357
}
271358

272359
return nil
273360
}
274361

275-
// awsAttributesToResourceData returns a map of valid values for a terraform schema from an attributes map and a conversion map
276362
func awsAttributesToResourceData(attributes map[string]string, resourceSchemas map[string]*schema.Schema, attributesToResourceMap map[string]string) (map[string]any, error) {
277363
values := make(map[string]any)
278364

internal/services/mnq/helpers_mnq_queue.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ var SQSAttributesToResourceMap = map[string]string{
155155
string(awstype.QueueAttributeNameContentBasedDeduplication): "content_based_deduplication",
156156
string(awstype.QueueAttributeNameReceiveMessageWaitTimeSeconds): "receive_wait_time_seconds",
157157
string(awstype.QueueAttributeNameVisibilityTimeout): "visibility_timeout_seconds",
158+
string(awstype.QueueAttributeNameRedrivePolicy): "dead_letter_queue",
159+
string(awstype.QueueAttributeNameQueueArn): "arn",
158160
}
159161

160162
// Returns all managed SQS attribute names
@@ -208,6 +210,31 @@ func resourceMNQQueueCustomizeDiff(_ context.Context, d *schema.ResourceDiff, _
208210
return errors.New("content-based deduplication can only be set for FIFO queue")
209211
}
210212

213+
// Validate dead-letter queue configuration
214+
if deadLetterConfig, ok := d.GetOk("dead_letter_queue"); ok {
215+
deadLetterList := deadLetterConfig.([]any)
216+
if len(deadLetterList) > 0 {
217+
config := deadLetterList[0].(map[string]any)
218+
queueID := config["id"].(string)
219+
maxReceiveCount := config["max_receive_count"].(int)
220+
221+
// Skip validation if queueID is empty or contains a reference
222+
// This means the reference hasn't been resolved yet during planning
223+
if queueID == "" || strings.Contains(queueID, "scaleway_mnq_sqs_queue") {
224+
// Reference not resolved yet, skip validation
225+
return nil
226+
}
227+
228+
if queueID == "" {
229+
return errors.New("dead-letter queue ID cannot be empty")
230+
}
231+
232+
if maxReceiveCount < 1 || maxReceiveCount > 1000 {
233+
return errors.New("max_receive_count must be between 1 and 1,000")
234+
}
235+
}
236+
}
237+
211238
if !nameRegex.MatchString(name) {
212239
return fmt.Errorf("invalid queue name: %s (format is %s)", name, nameRegex.String())
213240
}

internal/services/mnq/sqs_queue.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ func ResourceSQSQueue() *schema.Resource {
106106
ValidateFunc: validation.IntBetween(1024, 262_144),
107107
Description: "The maximum size of a message. Should be in bytes.",
108108
},
109+
"dead_letter_queue": {
110+
Type: schema.TypeList,
111+
Optional: true,
112+
MaxItems: 1,
113+
Description: "Configuration for the dead-letter queue",
114+
Elem: &schema.Resource{
115+
Schema: map[string]*schema.Schema{
116+
"id": {
117+
Type: schema.TypeString,
118+
Required: true,
119+
Description: "The ID or ARN of the dead-letter queue where messages are sent after the maximum receive count is exceeded.",
120+
},
121+
"max_receive_count": {
122+
Type: schema.TypeInt,
123+
Required: true,
124+
ValidateFunc: validation.IntBetween(1, 1000),
125+
Description: "The number of times a message is delivered to the source queue before being sent to the dead-letter queue. Must be between 1 and 1,000.",
126+
},
127+
},
128+
},
129+
},
109130
"region": regional.Schema(),
110131
"project_id": account.ProjectIDSchema(),
111132

@@ -116,6 +137,11 @@ func ResourceSQSQueue() *schema.Resource {
116137
Computed: true,
117138
Description: "The URL of the queue",
118139
},
140+
"arn": {
141+
Type: schema.TypeString,
142+
Computed: true,
143+
Description: "The ARN of the queue",
144+
},
119145
},
120146
CustomizeDiff: resourceMNQQueueCustomizeDiff,
121147
StateUpgraders: []schema.StateUpgrader{

internal/services/mnq/sqs_queue_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package mnq_test
33
import (
44
"context"
55
"fmt"
6+
"regexp"
67
"testing"
78

89
"github.com/aws/aws-sdk-go-v2/aws"
@@ -170,6 +171,131 @@ func TestAccSQSQueue_DefaultProject(t *testing.T) {
170171
})
171172
}
172173

174+
func TestAccSQSQueue_DeadLetterQueue(t *testing.T) {
175+
tt := acctest.NewTestTools(t)
176+
defer tt.Cleanup()
177+
178+
ctx := t.Context()
179+
180+
resource.ParallelTest(t, resource.TestCase{
181+
PreCheck: func() { acctest.PreCheck(t) },
182+
ProviderFactories: tt.ProviderFactories,
183+
CheckDestroy: isSQSQueueDestroyed(ctx, tt),
184+
Steps: []resource.TestStep{
185+
{
186+
Config: `
187+
resource scaleway_account_project main {
188+
name = "tf_tests_mnq_sqs_queue_dead_letter"
189+
}
190+
191+
resource scaleway_mnq_sqs main {
192+
project_id = scaleway_account_project.main.id
193+
}
194+
195+
resource scaleway_mnq_sqs_credentials main {
196+
project_id = scaleway_mnq_sqs.main.project_id
197+
permissions {
198+
can_manage = true
199+
}
200+
}
201+
202+
resource scaleway_mnq_sqs_queue dead_letter_queue {
203+
project_id = scaleway_mnq_sqs.main.project_id
204+
name = "dead-letter-queue"
205+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
206+
access_key = scaleway_mnq_sqs_credentials.main.access_key
207+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
208+
}
209+
210+
resource scaleway_mnq_sqs_queue main {
211+
project_id = scaleway_mnq_sqs.main.project_id
212+
name = "test-mnq-sqs-queue-dead-letter"
213+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
214+
access_key = scaleway_mnq_sqs_credentials.main.access_key
215+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
216+
217+
dead_letter_queue {
218+
id = scaleway_mnq_sqs_queue.dead_letter_queue.id
219+
max_receive_count = 3
220+
}
221+
222+
depends_on = [scaleway_mnq_sqs_queue.dead_letter_queue]
223+
}
224+
`,
225+
Check: resource.ComposeTestCheckFunc(
226+
isSQSQueuePresent(ctx, tt, "scaleway_mnq_sqs_queue.main"),
227+
isSQSQueuePresent(ctx, tt, "scaleway_mnq_sqs_queue.dead_letter_queue"),
228+
acctest.CheckResourceAttrUUID("scaleway_mnq_sqs_queue.main", "id"),
229+
acctest.CheckResourceAttrUUID("scaleway_mnq_sqs_queue.dead_letter_queue", "id"),
230+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.main", "name", "test-mnq-sqs-queue-dead-letter"),
231+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.dead_letter_queue", "name", "dead-letter-queue"),
232+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.main", "dead_letter_queue.0.max_receive_count", "3"),
233+
// Test that ARN is computed and follows expected format
234+
resource.TestCheckResourceAttrSet("scaleway_mnq_sqs_queue.main", "arn"),
235+
resource.TestCheckResourceAttrSet("scaleway_mnq_sqs_queue.dead_letter_queue", "arn"),
236+
resource.TestMatchResourceAttr("scaleway_mnq_sqs_queue.main", "arn", regexp.MustCompile(`^arn:scw:sqs:.*:.*:.*$`)),
237+
resource.TestMatchResourceAttr("scaleway_mnq_sqs_queue.dead_letter_queue", "arn", regexp.MustCompile(`^arn:scw:sqs:.*:.*:.*$`)),
238+
),
239+
},
240+
{
241+
Config: `
242+
resource scaleway_account_project main {
243+
name = "tf_tests_mnq_sqs_queue_dead_letter"
244+
}
245+
246+
resource scaleway_mnq_sqs main {
247+
project_id = scaleway_account_project.main.id
248+
}
249+
250+
resource scaleway_mnq_sqs_credentials main {
251+
project_id = scaleway_mnq_sqs.main.project_id
252+
permissions {
253+
can_manage = true
254+
}
255+
}
256+
257+
resource scaleway_mnq_sqs_queue dead_letter_queue {
258+
project_id = scaleway_mnq_sqs.main.project_id
259+
name = "dead-letter-queue"
260+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
261+
access_key = scaleway_mnq_sqs_credentials.main.access_key
262+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
263+
}
264+
265+
resource scaleway_mnq_sqs_queue main {
266+
project_id = scaleway_mnq_sqs.main.project_id
267+
name = "test-mnq-sqs-queue-dead-letter"
268+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
269+
access_key = scaleway_mnq_sqs_credentials.main.access_key
270+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
271+
272+
dead_letter_queue {
273+
id = scaleway_mnq_sqs_queue.dead_letter_queue.id
274+
max_receive_count = 5
275+
}
276+
277+
depends_on = [scaleway_mnq_sqs_queue.dead_letter_queue]
278+
}
279+
`,
280+
Check: resource.ComposeTestCheckFunc(
281+
isSQSQueuePresent(ctx, tt, "scaleway_mnq_sqs_queue.main"),
282+
isSQSQueuePresent(ctx, tt, "scaleway_mnq_sqs_queue.dead_letter_queue"),
283+
acctest.CheckResourceAttrUUID("scaleway_mnq_sqs_queue.main", "id"),
284+
acctest.CheckResourceAttrUUID("scaleway_mnq_sqs_queue.dead_letter_queue", "id"),
285+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.main", "name", "test-mnq-sqs-queue-dead-letter"),
286+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.dead_letter_queue", "name", "dead-letter-queue"),
287+
resource.TestCheckResourceAttr("scaleway_mnq_sqs_queue.main", "dead_letter_queue.0.max_receive_count", "5"),
288+
// Test that ARN is computed and follows expected format
289+
resource.TestCheckResourceAttrSet("scaleway_mnq_sqs_queue.main", "arn"),
290+
resource.TestCheckResourceAttrSet("scaleway_mnq_sqs_queue.dead_letter_queue", "arn"),
291+
resource.TestMatchResourceAttr("scaleway_mnq_sqs_queue.main", "arn", regexp.MustCompile(`^arn:scw:sqs:.*:.*:.*$`)),
292+
resource.TestMatchResourceAttr("scaleway_mnq_sqs_queue.dead_letter_queue", "arn", regexp.MustCompile(`^arn:scw:sqs:.*:.*:.*$`)),
293+
),
294+
},
295+
},
296+
})
297+
}
298+
173299
func isSQSQueuePresent(ctx context.Context, tt *acctest.TestTools, n string) resource.TestCheckFunc {
174300
return func(state *terraform.State) error {
175301
rs, ok := state.RootModule().Resources[n]

0 commit comments

Comments
 (0)