Skip to content

Commit 12d143f

Browse files
committed
feat(rdb): support per-rule descriptions in acl add and update docs
1 parent 6fd84f2 commit 12d143f

File tree

3 files changed

+115
-41
lines changed

3 files changed

+115
-41
lines changed

docs/commands/rdb.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,17 @@ Add an additional ACL rule to a Database Instance.
9595
**Usage:**
9696

9797
```
98-
scw rdb acl add <acl-rule-ips ...> [arg=value ...]
98+
scw rdb acl add [arg=value ...]
9999
```
100100

101101

102102
**Args:**
103103

104104
| Name | | Description |
105105
|------|---|-------------|
106-
| acl-rule-ips | Required | IP addresses defined in the ACL rules of the Database Instance |
107106
| 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. |
107+
| rules.{index}.ip | | IP addresses defined in the ACL rules of the Database Instance |
108+
| rules.{index}.description | | Description of the ACL rule. Use rules.0.description, rules.1.description, etc. to specify individual descriptions for each rule. |
109109
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |
110110

111111

internal/namespaces/rdb/v1/custom_acl.go

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ 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
2625
}
2726

2827
type CustomACLResult struct {
@@ -45,28 +44,29 @@ func rdbACLCustomResultMarshalerFunc(i any, opt *human.MarshalOpt) (string, erro
4544
}
4645

4746
func aclAddBuilder(c *core.Command) *core.Command {
48-
c.ArgsType = reflect.TypeOf(rdbACLCustomArgs{})
47+
c.ArgsType = reflect.TypeOf(rdb.AddInstanceACLRulesRequest{})
4948
c.ArgSpecs = core.ArgSpecs{
50-
{
51-
Name: "acl-rule-ips",
52-
Short: "IP addresses defined in the ACL rules of the Database Instance",
53-
Required: true,
54-
Positional: true,
55-
},
5649
{
5750
Name: "instance-id",
5851
Short: "ID of the Database Instance",
5952
Required: true,
6053
Positional: false,
6154
},
6255
{
63-
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.",
56+
Name: "rules.{index}.ip",
57+
Short: "IP addresses defined in the ACL rules of the Database Instance",
58+
Required: false,
59+
Positional: false,
60+
},
61+
{
62+
Name: "rules.{index}.description",
63+
Short: "Description of the ACL rule. Use rules.0.description, rules.1.description, etc. to specify individual descriptions for each rule.",
6564
Required: false,
6665
Positional: false,
6766
},
6867
core.RegionArgSpec(),
6968
}
69+
c.AcceptMultiplePositionalArgs = true
7070

7171
c.Interceptor = func(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
7272
respI, err := runner(ctx, argsI)
@@ -78,39 +78,40 @@ func aclAddBuilder(c *core.Command) *core.Command {
7878
}
7979

8080
c.Run = func(ctx context.Context, argsI any) (i any, e error) {
81-
args := argsI.(*rdbACLCustomArgs)
81+
args := argsI.(*rdb.AddInstanceACLRulesRequest)
8282
client := core.ExtractClient(ctx)
8383
api := rdb.NewAPI(client)
8484

85-
description := args.Description
86-
if description == "" {
87-
description = "Allow " + args.ACLRuleIPs.String()
85+
// Set default descriptions for rules that don't have one
86+
for i, rule := range args.Rules {
87+
if rule.Description == "" {
88+
args.Rules[i].Description = "Allow " + rule.IP.String()
89+
}
8890
}
8991

90-
rule, err := api.AddInstanceACLRules(&rdb.AddInstanceACLRulesRequest{
91-
Region: args.Region,
92-
InstanceID: args.InstanceID,
93-
Rules: []*rdb.ACLRuleRequest{
94-
{
95-
IP: args.ACLRuleIPs,
96-
Description: description,
97-
},
98-
},
99-
}, scw.WithContext(ctx))
92+
rule, err := api.AddInstanceACLRules(args, scw.WithContext(ctx))
10093
if err != nil {
10194
return nil, fmt.Errorf("failed to add ACL rule: %w", err)
10295
}
10396

97+
// Create success message
98+
var message string
99+
if len(args.Rules) == 1 {
100+
message = fmt.Sprintf("ACL rule %s successfully added", args.Rules[0].IP.String())
101+
} else {
102+
message = fmt.Sprintf("%d ACL rules successfully added", len(args.Rules))
103+
}
104+
104105
return &CustomACLResult{
105106
Rules: rule.Rules,
106107
Success: core.SuccessResult{
107-
Message: fmt.Sprintf("ACL rule %s successfully added", args.ACLRuleIPs.String()),
108+
Message: message,
108109
},
109110
}, nil
110111
}
111112

112113
c.WaitFunc = func(ctx context.Context, argsI, respI any) (any, error) {
113-
args := argsI.(*rdbACLCustomArgs)
114+
args := argsI.(*rdb.AddInstanceACLRulesRequest)
114115
api := rdb.NewAPI(core.ExtractClient(ctx))
115116

116117
_, err := api.WaitForInstance(&rdb.WaitForInstanceRequest{
@@ -146,6 +147,7 @@ func aclDeleteBuilder(c *core.Command) *core.Command {
146147
},
147148
core.RegionArgSpec(),
148149
}
150+
c.AcceptMultiplePositionalArgs = true
149151

150152
c.Interceptor = func(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
151153
respI, err := runner(ctx, argsI)
@@ -175,34 +177,55 @@ func aclDeleteBuilder(c *core.Command) *core.Command {
175177

176178
// 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
177179
// before deleting it to warn them if nothing was done
178-
ruleWasSet := false
179180
rules, err := api.ListInstanceACLRules(&rdb.ListInstanceACLRulesRequest{
180181
Region: args.Region,
181182
InstanceID: args.InstanceID,
182183
}, scw.WithContext(ctx), scw.WithAllPages())
183184
if err != nil {
184185
return nil, fmt.Errorf("failed to list ACL rules: %w", err)
185186
}
187+
188+
// Check which rules were actually set
189+
existingIPs := make(map[string]bool)
186190
for _, rule := range rules.Rules {
187-
if rule.IP.String() == args.ACLRuleIPs.String() {
188-
ruleWasSet = true
189-
}
191+
existingIPs[rule.IP.String()] = true
192+
}
193+
194+
// Convert IPs to strings for deletion
195+
ipStrings := make([]string, len(args.ACLRuleIPs))
196+
for i, ip := range args.ACLRuleIPs {
197+
ipStrings[i] = ip.String()
190198
}
191199

192200
_, err = api.DeleteInstanceACLRules(&rdb.DeleteInstanceACLRulesRequest{
193201
Region: args.Region,
194202
InstanceID: args.InstanceID,
195-
ACLRuleIPs: []string{args.ACLRuleIPs.String()},
203+
ACLRuleIPs: ipStrings,
196204
}, scw.WithContext(ctx))
197205
if err != nil {
198-
return nil, fmt.Errorf("failed to remove ACL rule: %w", err)
206+
return nil, fmt.Errorf("failed to remove ACL rules: %w", err)
207+
}
208+
209+
// Count how many rules were actually deleted
210+
deletedCount := 0
211+
for _, ip := range args.ACLRuleIPs {
212+
if existingIPs[ip.String()] {
213+
deletedCount++
214+
}
199215
}
200216

201217
var message string
202-
if ruleWasSet {
203-
message = fmt.Sprintf("ACL rule %s successfully deleted", args.ACLRuleIPs.String())
218+
if len(args.ACLRuleIPs) == 1 {
219+
if deletedCount > 0 {
220+
message = fmt.Sprintf("ACL rule %s successfully deleted", args.ACLRuleIPs[0].String())
221+
} else {
222+
message = fmt.Sprintf("ACL rule %s was not set", args.ACLRuleIPs[0].String())
223+
}
204224
} else {
205-
message = fmt.Sprintf("ACL rule %s was not set", args.ACLRuleIPs.String())
225+
message = fmt.Sprintf("%d ACL rules successfully deleted", deletedCount)
226+
if deletedCount < len(args.ACLRuleIPs) {
227+
message += fmt.Sprintf(" (%d were not set)", len(args.ACLRuleIPs)-deletedCount)
228+
}
206229
}
207230

208231
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.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.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)