Skip to content

Commit f97b0a8

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

File tree

9 files changed

+6032
-8283
lines changed

9 files changed

+6032
-8283
lines changed

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: 80 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,51 @@ 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+
var scwARN string
236+
237+
switch {
238+
case strings.HasPrefix(queueID, "arn:scw:sqs:"):
239+
scwARN = queueID
240+
case strings.Contains(queueID, "/"):
241+
parts := strings.Split(queueID, "/")
242+
if len(parts) == 3 {
243+
region := parts[0]
244+
projectID := parts[1]
245+
queueName := parts[2]
246+
247+
scwARN = fmt.Sprintf("arn:scw:sqs:%s:project-%s:%s", region, projectID, queueName)
248+
} else {
249+
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)
250+
}
251+
default:
252+
scwARN = queueID
253+
}
254+
255+
// Create RedrivePolicy JSON
256+
redrivePolicy := map[string]any{
257+
"deadLetterTargetArn": scwARN,
258+
"maxReceiveCount": maxReceiveCount,
259+
}
260+
261+
jsonData, err := json.Marshal(redrivePolicy)
262+
if err != nil {
263+
return fmt.Errorf("failed to marshal redrive policy: %w", err)
264+
}
265+
266+
s = string(jsonData)
267+
}
268+
} else {
269+
return fmt.Errorf("unsupported list type for %s", resourcePath)
270+
}
227271
default:
228272
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
229273
}
@@ -265,14 +309,47 @@ func awsAttributeToResourceData(values map[string]any, value string, resourcePat
265309
setResourceValue(values, resourcePath, i, resourceSchemas)
266310
case schema.TypeString:
267311
setResourceValue(values, resourcePath, value, resourceSchemas)
312+
case schema.TypeList:
313+
if resourcePath == "dead_letter_queue" && value != "" {
314+
var redrivePolicy map[string]any
315+
if err := json.Unmarshal([]byte(value), &redrivePolicy); err != nil {
316+
return fmt.Errorf("failed to unmarshal redrive policy: %w", err)
317+
}
318+
319+
deadLetterTargetArn := redrivePolicy["deadLetterTargetArn"].(string)
320+
321+
var terraformID string
322+
323+
if strings.HasPrefix(deadLetterTargetArn, "arn:scw:sqs:") {
324+
parts := strings.Split(deadLetterTargetArn, ":")
325+
if len(parts) >= 6 {
326+
region := parts[3]
327+
projectID := strings.TrimPrefix(parts[4], "project-")
328+
queueName := parts[5]
329+
terraformID = fmt.Sprintf("%s/%s/%s", region, projectID, queueName)
330+
} else {
331+
terraformID = deadLetterTargetArn
332+
}
333+
} else {
334+
terraformID = deadLetterTargetArn
335+
}
336+
337+
deadLetterConfig := map[string]any{
338+
"id": terraformID,
339+
"max_receive_count": int(redrivePolicy["maxReceiveCount"].(float64)),
340+
}
341+
342+
setResourceValue(values, resourcePath, []any{deadLetterConfig}, resourceSchemas)
343+
} else {
344+
return fmt.Errorf("unsupported list type for %s", resourcePath)
345+
}
268346
default:
269347
return fmt.Errorf("unsupported type %s for %s", resourceSchema.Type, resourcePath)
270348
}
271349

272350
return nil
273351
}
274352

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

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{

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)