Skip to content

Commit 704ebe3

Browse files
jremy42remyleone
andauthored
feat(rdb): support per-rule descriptions in acl add (#5017)
Co-authored-by: Rémy Léone <[email protected]>
1 parent 4995e6c commit 704ebe3

28 files changed

+17614
-5924
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
version: 1
3+
interactions:
4+
- request:
5+
body: '{"message":"authentication is denied","method":"api_key","reason":"not_found","type":"denied_authentication"}'
6+
form: {}
7+
headers:
8+
User-Agent:
9+
- scaleway-sdk-go/v1.0.0-beta.35.0.20250917154444-1d3cdbf4ce0d (go1.24.6; darwin;
10+
amd64) cli-e2e-test
11+
url: https://api.scaleway.com/iam/v1alpha1/api-keys/SCWXXXXXXXXXXXXXXXXX
12+
method: GET
13+
response:
14+
body: '{"message":"authentication is denied","method":"api_key","reason":"not_found","type":"denied_authentication"}'
15+
headers:
16+
Content-Length:
17+
- "109"
18+
Content-Security-Policy:
19+
- default-src 'none'; frame-ancestors 'none'
20+
Content-Type:
21+
- application/json
22+
Date:
23+
- Mon, 29 Sep 2025 08:31:55 GMT
24+
Server:
25+
- Scaleway API Gateway (fr-par-1;edge01)
26+
Strict-Transport-Security:
27+
- max-age=63072000
28+
X-Content-Type-Options:
29+
- nosniff
30+
X-Frame-Options:
31+
- DENY
32+
X-Request-Id:
33+
- 32b2b5f8-48c2-4aef-b89e-e8da1b64cd32
34+
status: 401 Unauthorized
35+
code: 401
36+
duration: ""

cmd/scw/testdata/test-all-usage-rdb-acl-add-usage.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ USAGE:
88
ARGS:
99
acl-rule-ips IP addresses defined in the ACL rules of the Database Instance
1010
instance-id ID of the Database Instance
11-
[description] Description of the ACL rule. Indexes are not yet supported so the description will be applied to all the rules of the command.
11+
[description] Description of the ACL rule. If multiple IPs are provided, this description will be applied to all rules unless specific descriptions are provided.
12+
[descriptions] Descriptions of the ACL rules
1213
[region=fr-par] Region to target. If none is passed will use default region from the config
1314

1415
FLAGS:

docs/commands/rdb.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ scw rdb acl add <acl-rule-ips ...> [arg=value ...]
103103

104104
| Name | | Description |
105105
|------|---|-------------|
106-
| acl-rule-ips | Required | IP addresses defined in the ACL rules of the Database Instance |
106+
| acl-rule-ips | | IP addresses defined in the ACL rules of the Database Instance |
107107
| instance-id | Required | ID of the Database Instance |
108-
| description | | Description of the ACL rule. Indexes are not yet supported so the description will be applied to all the rules of the command. |
108+
| description | | Description of the ACL rule. If multiple IPs are provided, this description will be applied to all rules unless specific descriptions are provided. |
109+
| descriptions | | Descriptions of the ACL rules |
109110
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |
110111

111112

@@ -125,7 +126,7 @@ scw rdb acl delete <acl-rule-ips ...> [arg=value ...]
125126

126127
| Name | | Description |
127128
|------|---|-------------|
128-
| acl-rule-ips | Required | IP addresses defined in the ACL rules of the Database Instance |
129+
| acl-rule-ips | | IP addresses defined in the ACL rules of the Database Instance |
129130
| instance-id | Required | ID of the Database Instance |
130131
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |
131132

internal/namespaces/rdb/v1/custom_acl.go

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ var aclRuleActionMarshalSpecs = human.EnumMarshalSpecs{
1919
}
2020

2121
type rdbACLCustomArgs struct {
22-
Region scw.Region
23-
InstanceID string
24-
ACLRuleIPs scw.IPNet
25-
Description string
22+
Region scw.Region
23+
InstanceID string
24+
ACLRuleIPs []scw.IPNet
25+
}
26+
27+
type rdbACLAddCustomArgs struct {
28+
Region scw.Region
29+
InstanceID string
30+
ACLRuleIPs []scw.IPNet
31+
Description string
32+
Descriptions []string
2633
}
2734

2835
type CustomACLResult struct {
@@ -45,12 +52,12 @@ func rdbACLCustomResultMarshalerFunc(i any, opt *human.MarshalOpt) (string, erro
4552
}
4653

4754
func aclAddBuilder(c *core.Command) *core.Command {
48-
c.ArgsType = reflect.TypeOf(rdbACLCustomArgs{})
55+
c.ArgsType = reflect.TypeOf(rdbACLAddCustomArgs{})
4956
c.ArgSpecs = core.ArgSpecs{
5057
{
5158
Name: "acl-rule-ips",
5259
Short: "IP addresses defined in the ACL rules of the Database Instance",
53-
Required: true,
60+
Required: false,
5461
Positional: true,
5562
},
5663
{
@@ -61,12 +68,19 @@ func aclAddBuilder(c *core.Command) *core.Command {
6168
},
6269
{
6370
Name: "description",
64-
Short: "Description of the ACL rule. Indexes are not yet supported so the description will be applied to all the rules of the command.",
71+
Short: "Description of the ACL rule. If multiple IPs are provided, this description will be applied to all rules unless specific descriptions are provided.",
72+
Required: false,
73+
Positional: false,
74+
},
75+
{
76+
Name: "descriptions",
77+
Short: "Descriptions of the ACL rules",
6578
Required: false,
6679
Positional: false,
6780
},
6881
core.RegionArgSpec(),
6982
}
83+
c.AcceptMultiplePositionalArgs = true
7084

7185
c.Interceptor = func(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
7286
respI, err := runner(ctx, argsI)
@@ -78,39 +92,53 @@ func aclAddBuilder(c *core.Command) *core.Command {
7892
}
7993

8094
c.Run = func(ctx context.Context, argsI any) (i any, e error) {
81-
args := argsI.(*rdbACLCustomArgs)
95+
args := argsI.(*rdbACLAddCustomArgs)
8296
client := core.ExtractClient(ctx)
8397
api := rdb.NewAPI(client)
8498

85-
description := args.Description
86-
if description == "" {
87-
description = "Allow " + args.ACLRuleIPs.String()
99+
// Build rules with general and specific descriptions
100+
rules := make([]*rdb.ACLRuleRequest, 0, len(args.ACLRuleIPs))
101+
for i, ip := range args.ACLRuleIPs {
102+
description := args.Description
103+
if description == "" {
104+
description = "Allow " + ip.String()
105+
}
106+
if i < len(args.Descriptions) && args.Descriptions[i] != "" {
107+
description = args.Descriptions[i]
108+
}
109+
rules = append(rules, &rdb.ACLRuleRequest{
110+
IP: ip,
111+
Description: description,
112+
})
88113
}
89114

90115
rule, err := api.AddInstanceACLRules(&rdb.AddInstanceACLRulesRequest{
91116
Region: args.Region,
92117
InstanceID: args.InstanceID,
93-
Rules: []*rdb.ACLRuleRequest{
94-
{
95-
IP: args.ACLRuleIPs,
96-
Description: description,
97-
},
98-
},
118+
Rules: rules,
99119
}, scw.WithContext(ctx))
100120
if err != nil {
101121
return nil, fmt.Errorf("failed to add ACL rule: %w", err)
102122
}
103123

124+
// Create success message
125+
var message string
126+
if len(args.ACLRuleIPs) == 1 {
127+
message = fmt.Sprintf("ACL rule %s successfully added", args.ACLRuleIPs[0].String())
128+
} else {
129+
message = fmt.Sprintf("%d ACL rules successfully added", len(args.ACLRuleIPs))
130+
}
131+
104132
return &CustomACLResult{
105133
Rules: rule.Rules,
106134
Success: core.SuccessResult{
107-
Message: fmt.Sprintf("ACL rule %s successfully added", args.ACLRuleIPs.String()),
135+
Message: message,
108136
},
109137
}, nil
110138
}
111139

112140
c.WaitFunc = func(ctx context.Context, argsI, respI any) (any, error) {
113-
args := argsI.(*rdbACLCustomArgs)
141+
args := argsI.(*rdbACLAddCustomArgs)
114142
api := rdb.NewAPI(core.ExtractClient(ctx))
115143

116144
_, err := api.WaitForInstance(&rdb.WaitForInstanceRequest{
@@ -135,7 +163,7 @@ func aclDeleteBuilder(c *core.Command) *core.Command {
135163
{
136164
Name: "acl-rule-ips",
137165
Short: "IP addresses defined in the ACL rules of the Database Instance",
138-
Required: true,
166+
Required: false,
139167
Positional: true,
140168
},
141169
{
@@ -146,6 +174,7 @@ func aclDeleteBuilder(c *core.Command) *core.Command {
146174
},
147175
core.RegionArgSpec(),
148176
}
177+
c.AcceptMultiplePositionalArgs = true
149178

150179
c.Interceptor = func(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
151180
respI, err := runner(ctx, argsI)
@@ -175,34 +204,58 @@ func aclDeleteBuilder(c *core.Command) *core.Command {
175204

176205
// The API returns 200 OK even if the rule was not set in the first place, so we have to check if the rule was present
177206
// before deleting it to warn them if nothing was done
178-
ruleWasSet := false
179207
rules, err := api.ListInstanceACLRules(&rdb.ListInstanceACLRulesRequest{
180208
Region: args.Region,
181209
InstanceID: args.InstanceID,
182210
}, scw.WithContext(ctx), scw.WithAllPages())
183211
if err != nil {
184212
return nil, fmt.Errorf("failed to list ACL rules: %w", err)
185213
}
214+
215+
// Check which rules were actually set
216+
existingIPs := make(map[string]bool)
186217
for _, rule := range rules.Rules {
187-
if rule.IP.String() == args.ACLRuleIPs.String() {
188-
ruleWasSet = true
189-
}
218+
existingIPs[rule.IP.String()] = true
219+
}
220+
221+
// Convert IPs to strings for deletion
222+
ipStrings := make([]string, len(args.ACLRuleIPs))
223+
for i, ip := range args.ACLRuleIPs {
224+
ipStrings[i] = ip.String()
190225
}
191226

192227
_, err = api.DeleteInstanceACLRules(&rdb.DeleteInstanceACLRulesRequest{
193228
Region: args.Region,
194229
InstanceID: args.InstanceID,
195-
ACLRuleIPs: []string{args.ACLRuleIPs.String()},
230+
ACLRuleIPs: ipStrings,
196231
}, scw.WithContext(ctx))
197232
if err != nil {
198-
return nil, fmt.Errorf("failed to remove ACL rule: %w", err)
233+
return nil, fmt.Errorf("failed to remove ACL rules: %w", err)
234+
}
235+
236+
// Count how many rules were actually deleted
237+
deletedCount := 0
238+
for _, ip := range args.ACLRuleIPs {
239+
if existingIPs[ip.String()] {
240+
deletedCount++
241+
}
199242
}
200243

201244
var message string
202-
if ruleWasSet {
203-
message = fmt.Sprintf("ACL rule %s successfully deleted", args.ACLRuleIPs.String())
245+
if len(args.ACLRuleIPs) == 1 {
246+
if deletedCount > 0 {
247+
message = fmt.Sprintf(
248+
"ACL rule %s successfully deleted",
249+
args.ACLRuleIPs[0].String(),
250+
)
251+
} else {
252+
message = fmt.Sprintf("ACL rule %s was not set", args.ACLRuleIPs[0].String())
253+
}
204254
} else {
205-
message = fmt.Sprintf("ACL rule %s was not set", args.ACLRuleIPs.String())
255+
message = fmt.Sprintf("%d ACL rules successfully deleted", deletedCount)
256+
if deletedCount < len(args.ACLRuleIPs) {
257+
message += fmt.Sprintf(" (%d were not set)", len(args.ACLRuleIPs)-deletedCount)
258+
}
206259
}
207260

208261
return &CustomACLResult{

internal/namespaces/rdb/v1/custom_acl_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,57 @@ func Test_SetACL(t *testing.T) {
195195
),
196196
AfterFunc: deleteInstance(),
197197
}))
198+
199+
t.Run("Multiple with individual descriptions", core.Test(&core.TestConfig{
200+
Commands: rdb.GetCommands(),
201+
BeforeFunc: core.BeforeFuncCombine(
202+
fetchLatestEngine("PostgreSQL"),
203+
createInstance("{{.latestEngine}}"),
204+
),
205+
Cmd: "scw rdb acl add 1.1.1.1 2.2.2.2 3.3.3.3 instance-id={{ .Instance.ID }} descriptions.0=first descriptions.1=second descriptions.2=third --wait",
206+
Check: core.TestCheckCombine(
207+
core.TestCheckGolden(),
208+
func(t *testing.T, ctx *core.CheckFuncCtx) {
209+
t.Helper()
210+
verifyACL(t, ctx, []string{"0.0.0.0/0", "1.1.1.1/32", "2.2.2.2/32", "3.3.3.3/32"})
211+
},
212+
),
213+
AfterFunc: deleteInstance(),
214+
}))
215+
216+
t.Run("Multiple with partial descriptions", core.Test(&core.TestConfig{
217+
Commands: rdb.GetCommands(),
218+
BeforeFunc: core.BeforeFuncCombine(
219+
fetchLatestEngine("PostgreSQL"),
220+
createInstance("{{.latestEngine}}"),
221+
),
222+
Cmd: "scw rdb acl add 1.1.1.1 2.2.2.2 3.3.3.3 instance-id={{ .Instance.ID }} descriptions.0=first descriptions.1=second descriptions.2=third --wait",
223+
Check: core.TestCheckCombine(
224+
core.TestCheckGolden(),
225+
func(t *testing.T, ctx *core.CheckFuncCtx) {
226+
t.Helper()
227+
verifyACL(t, ctx, []string{"0.0.0.0/0", "1.1.1.1/32", "2.2.2.2/32", "3.3.3.3/32"})
228+
},
229+
),
230+
AfterFunc: deleteInstance(),
231+
}))
232+
233+
t.Run("Multiple with general description and specific descriptions", core.Test(&core.TestConfig{
234+
Commands: rdb.GetCommands(),
235+
BeforeFunc: core.BeforeFuncCombine(
236+
fetchLatestEngine("PostgreSQL"),
237+
createInstance("{{.latestEngine}}"),
238+
),
239+
Cmd: "scw rdb acl add 1.1.1.1 2.2.2.2 3.3.3.3 instance-id={{ .Instance.ID }} description=default descriptions.0=first descriptions.1=second --wait",
240+
Check: core.TestCheckCombine(
241+
core.TestCheckGolden(),
242+
func(t *testing.T, ctx *core.CheckFuncCtx) {
243+
t.Helper()
244+
verifyACL(t, ctx, []string{"0.0.0.0/0", "1.1.1.1/32", "2.2.2.2/32", "3.3.3.3/32"})
245+
},
246+
),
247+
AfterFunc: deleteInstance(),
248+
}))
198249
}
199250

200251
func verifyACLCustomResponse(t *testing.T, res *rdb.CustomACLResult, expectedRules []string) {

0 commit comments

Comments
 (0)