Skip to content

Commit 79725fd

Browse files
authored
feat(mnq/sqs): support for dead_letter_queue (#3293)
1 parent 8083e14 commit 79725fd

File tree

11 files changed

+6132
-8283
lines changed

11 files changed

+6132
-8283
lines changed

docs/resources/mnq_sqs_queue.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,44 @@ resource "scaleway_mnq_sqs_queue" "main" {
3636
}
3737
```
3838

39+
### With Dead Letter Queue
40+
41+
```terraform
42+
resource "scaleway_mnq_sqs" "main" {}
43+
44+
resource "scaleway_mnq_sqs_credentials" "main" {
45+
project_id = scaleway_mnq_sqs.main.project_id
46+
name = "sqs-credentials"
47+
48+
permissions {
49+
can_manage = true
50+
can_receive = false
51+
can_publish = false
52+
}
53+
}
54+
55+
resource "scaleway_mnq_sqs_queue" "dead_letter" {
56+
project_id = scaleway_mnq_sqs.main.project_id
57+
name = "dead-letter-queue"
58+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
59+
access_key = scaleway_mnq_sqs_credentials.main.access_key
60+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
61+
}
62+
63+
resource "scaleway_mnq_sqs_queue" "main" {
64+
project_id = scaleway_mnq_sqs.main.project_id
65+
name = "my-queue"
66+
sqs_endpoint = scaleway_mnq_sqs.main.endpoint
67+
access_key = scaleway_mnq_sqs_credentials.main.access_key
68+
secret_key = scaleway_mnq_sqs_credentials.main.secret_key
69+
70+
dead_letter_queue {
71+
id = scaleway_mnq_sqs_queue.dead_letter.id
72+
max_receive_count = 3
73+
}
74+
}
75+
```
76+
3977
## Argument Reference
4078

4179
The following arguments are supported:
@@ -62,10 +100,20 @@ The following arguments are supported:
62100

63101
- `message_max_size` - (Optional) The maximum size of a message. Should be in bytes. Must be between 1024 and 262_144. Defaults to 262_144.
64102

103+
- `dead_letter_queue` - (Optional) Configuration for the dead letter queue. See [Dead Letter Queue](#dead-letter-queue) below for details.
104+
65105
- `region` - (Defaults to [provider](../index.md#region) `region`). The [region](../guides/regions_and_zones.md#regions) in which SQS is enabled.
66106

67107
- `project_id` - (Defaults to [provider](../index.md#project_id) `project_id`) The ID of the Project in which SQS is enabled.
68108

109+
## Dead Letter Queue
110+
111+
The `dead_letter_queue` block supports the following:
112+
113+
- `id` - (Required) The ID of the dead letter queue. Can be either in the format `{region}/{project-id}/{queue-name}` or `arn:scw:sqs:{region}:project-{project-id}:{queue-name}`.
114+
115+
- `max_receive_count` - (Required) The number of times a message is delivered to the source queue before being moved to the dead letter queue. Must be between 1 and 1000.
116+
69117

70118
## Attributes Reference
71119

internal/services/container/testdata/trigger-sqs.cassette.yaml

Lines changed: 1346 additions & 905 deletions
Large diffs are not rendered by default.

internal/services/function/testdata/function-trigger-sqs.cassette.yaml

Lines changed: 1213 additions & 625 deletions
Large diffs are not rendered by default.

internal/services/mnq/helpers_mnq.go

Lines changed: 84 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"
@@ -17,6 +18,12 @@ import (
1718
const (
1819
AWSErrQueueDeletedRecently = "AWS.SimpleQueueService.QueueDeletedRecently"
1920
AWSErrNonExistentQueue = "AWS.SimpleQueueService.NonExistentQueue"
21+
22+
// SQS ARN prefix
23+
SQSPrefix = "arn:scw:sqs:"
24+
25+
// Dead letter queue resource path
26+
DeadLetterQueuePath = "dead_letter_queue"
2027
)
2128

2229
func newMNQNatsAPI(d *schema.ResourceData, m any) (*mnq.NatsAPI, scw.Region, error) {
@@ -172,11 +179,9 @@ func ComposeSNSARN(region scw.Region, projectID string, resourceName string) str
172179
return composeARN("sns", region, projectID, resourceName)
173180
}
174181

175-
// Set the value inside values at the resource path (e.g. a.0.b sets b's value)
176182
func setResourceValue(values map[string]any, resourcePath string, value any, resourceSchemas map[string]*schema.Schema) {
177183
parts := strings.Split(resourcePath, ".")
178184
if len(parts) > 1 {
179-
// Terraform's nested objects are represented as slices of maps
180185
if _, ok := values[parts[0]]; !ok {
181186
values[parts[0]] = []any{make(map[string]any)}
182187
}
@@ -224,6 +229,49 @@ func awsResourceDataToAttribute(awsAttributes map[string]string, awsAttribute st
224229
s = strconv.Itoa(resourceValue.(int))
225230
case schema.TypeString:
226231
s = resourceValue.(string)
232+
case schema.TypeList:
233+
if resourcePath == DeadLetterQueuePath {
234+
deadLetterConfig := resourceValue.([]any)
235+
if len(deadLetterConfig) > 0 {
236+
config := deadLetterConfig[0].(map[string]any)
237+
queueID := config["id"].(string)
238+
maxReceiveCount := config["max_receive_count"].(int)
239+
240+
var scwARN string
241+
242+
switch {
243+
case strings.HasPrefix(queueID, SQSPrefix):
244+
scwARN = queueID
245+
case strings.Contains(queueID, "/"):
246+
parts := strings.Split(queueID, "/")
247+
if len(parts) == 3 {
248+
region := parts[0]
249+
projectID := parts[1]
250+
queueName := parts[2]
251+
252+
scwARN = fmt.Sprintf("arn:scw:sqs:%s:project-%s:%s", region, projectID, queueName)
253+
} else {
254+
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)
255+
}
256+
default:
257+
scwARN = queueID
258+
}
259+
260+
redrivePolicy := map[string]any{
261+
"deadLetterTargetArn": scwARN,
262+
"maxReceiveCount": maxReceiveCount,
263+
}
264+
265+
jsonData, err := json.Marshal(redrivePolicy)
266+
if err != nil {
267+
return fmt.Errorf("failed to marshal redrive policy: %w", err)
268+
}
269+
270+
s = string(jsonData)
271+
}
272+
} else {
273+
return fmt.Errorf("unsupported list type for %s", resourcePath)
274+
}
227275
default:
228276
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
229277
}
@@ -265,14 +313,47 @@ func awsAttributeToResourceData(values map[string]any, value string, resourcePat
265313
setResourceValue(values, resourcePath, i, resourceSchemas)
266314
case schema.TypeString:
267315
setResourceValue(values, resourcePath, value, resourceSchemas)
316+
case schema.TypeList:
317+
if resourcePath == DeadLetterQueuePath && value != "" {
318+
var redrivePolicy map[string]any
319+
if err := json.Unmarshal([]byte(value), &redrivePolicy); err != nil {
320+
return fmt.Errorf("failed to unmarshal redrive policy: %w", err)
321+
}
322+
323+
deadLetterTargetArn := redrivePolicy["deadLetterTargetArn"].(string)
324+
325+
var terraformID string
326+
327+
if strings.HasPrefix(deadLetterTargetArn, SQSPrefix) {
328+
parts := strings.Split(deadLetterTargetArn, ":")
329+
if len(parts) >= 6 {
330+
region := parts[3]
331+
projectID := strings.TrimPrefix(parts[4], "project-")
332+
queueName := parts[5]
333+
terraformID = fmt.Sprintf("%s/%s/%s", region, projectID, queueName)
334+
} else {
335+
terraformID = deadLetterTargetArn
336+
}
337+
} else {
338+
terraformID = deadLetterTargetArn
339+
}
340+
341+
deadLetterConfig := map[string]any{
342+
"id": terraformID,
343+
"max_receive_count": int(redrivePolicy["maxReceiveCount"].(float64)),
344+
}
345+
346+
setResourceValue(values, resourcePath, []any{deadLetterConfig}, resourceSchemas)
347+
} else {
348+
return fmt.Errorf("unsupported list type for %s", resourcePath)
349+
}
268350
default:
269351
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
270352
}
271353

272354
return nil
273355
}
274356

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

internal/services/mnq/helpers_mnq_queue.go

Lines changed: 24 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,28 @@ 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+
if queueID == "" || strings.Contains(queueID, "scaleway_mnq_sqs_queue") {
222+
return nil
223+
}
224+
225+
if queueID == "" {
226+
return errors.New("dead-letter queue ID cannot be empty")
227+
}
228+
229+
if maxReceiveCount < 1 || maxReceiveCount > 1000 {
230+
return errors.New("max_receive_count must be between 1 and 1,000")
231+
}
232+
}
233+
}
234+
211235
if !nameRegex.MatchString(name) {
212236
return fmt.Errorf("invalid queue name: %s (format is %s)", name, nameRegex.String())
213237
}

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{

0 commit comments

Comments
 (0)