@@ -2,19 +2,21 @@ package serverfirewall
22
33import (
44 "fmt"
5+ "sort"
56
67 "github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
78 "github.com/UpCloudLtd/upcloud-cli/v3/internal/completion"
89 "github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
910 "github.com/UpCloudLtd/upcloud-cli/v3/internal/resolver"
11+ "github.com/UpCloudLtd/upcloud-cli/v3/internal/ui"
12+ "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud"
1013 "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
11- "github.com/spf13/cobra"
1214 "github.com/spf13/pflag"
1315)
1416
1517type deleteCommand struct {
1618 * commands.BaseCommand
17- rulePosition int
19+ params ruleModifyParams
1820 completion.Server
1921 resolver.CachingServer
2022}
@@ -24,39 +26,118 @@ func DeleteCommand() commands.Command {
2426 return & deleteCommand {
2527 BaseCommand : commands .New (
2628 "delete" ,
27- "Removes a firewall rule from a server. Firewall rules must be removed individually. The positions of remaining firewall rules will be adjusted after a rule is removed ." ,
29+ "Delete firewall rules from a server. Rules can be deleted by position or by using filters ." ,
2830 "upctl server firewall delete 00038afc-d526-4148-af0e-d2f1eeaded9b --position 1" ,
31+ "upctl server firewall delete myserver --comment \" temporary rule\" " ,
32+ "upctl server firewall delete myserver --direction in --protocol tcp --dest-port 8080" ,
2933 ),
34+ params : ruleModifyParams {
35+ skipConfirmation : 1 ,
36+ },
3037 }
3138}
3239
3340// InitCommand implements Command.InitCommand
3441func (s * deleteCommand ) InitCommand () {
3542 flagSet := & pflag.FlagSet {}
36- flagSet . IntVar ( & s .rulePosition , "position" , 0 , "Rule position. Available: 1-1000" )
43+ addRuleFilterFlags ( flagSet , & s .params , s . Cobra () )
3744 s .AddFlags (flagSet )
38-
39- commands .Must (s .Cobra ().MarkFlagRequired ("position" ))
40- commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("position" , cobra .NoFileCompletions ))
45+ configureRuleFilterFlagsPostAdd (s .Cobra ())
4146}
4247
4348// Execute implements commands.MultipleArgumentCommand
4449func (s * deleteCommand ) Execute (exec commands.Executor , arg string ) (output.Output , error ) {
45- if s .rulePosition < 1 || s .rulePosition > 1000 {
46- return nil , fmt .Errorf ("invalid position (1-1000 allowed)" )
47- }
48- msg := fmt .Sprintf ("Deleting firewall rule %d from server %v" , s .rulePosition , arg )
49- exec .PushProgressStarted (msg )
50-
51- err := exec .Firewall ().DeleteFirewallRule (exec .Context (), & request.DeleteFirewallRuleRequest {
52- ServerUUID : arg ,
53- Position : s .rulePosition ,
50+ // Get current firewall rules
51+ server , err := exec .Server ().GetServerDetails (exec .Context (), & request.GetServerDetailsRequest {
52+ UUID : arg ,
5453 })
5554 if err != nil {
56- return commands .HandleError (exec , msg , err )
55+ return nil , err
56+ }
57+
58+ // Find matching rules
59+ matchedIndices := findMatchingRules (server .FirewallRules , & s .params )
60+
61+ if len (matchedIndices ) == 0 {
62+ return nil , fmt .Errorf ("no firewall rules matched the specified filters" )
5763 }
5864
59- exec .PushProgressSuccess (msg )
65+ // Confirm if multiple rules or if confirmation required
66+ if len (matchedIndices ) > s .params .skipConfirmation {
67+ exec .PushProgressUpdate (fmt .Sprintf ("Found %d matching firewall rules:" , len (matchedIndices )))
68+ for _ , idx := range matchedIndices {
69+ rule := & server .FirewallRules [idx ]
70+ exec .PushProgressUpdate (fmt .Sprintf (" Position %d: %s %s %s -> %s" ,
71+ rule .Position , rule .Direction , rule .Protocol ,
72+ formatRuleAddress (rule , true ), formatRuleAddress (rule , false )))
73+ }
74+
75+ if ! ui .Confirm (fmt .Sprintf ("Delete %d firewall rules?" , len (matchedIndices ))) {
76+ return output.None {}, nil
77+ }
78+ }
79+
80+ // Sort indices in descending order to delete from highest position first
81+ // This prevents position shifts affecting subsequent deletions
82+ sort .Sort (sort .Reverse (sort .IntSlice (matchedIndices )))
83+
84+ // Delete each matched rule
85+ deletedCount := 0
86+ for _ , idx := range matchedIndices {
87+ rule := & server .FirewallRules [idx ]
88+ msg := fmt .Sprintf ("Deleting firewall rule at position %d" , rule .Position )
89+ exec .PushProgressStarted (msg )
90+
91+ err := exec .Firewall ().DeleteFirewallRule (exec .Context (), & request.DeleteFirewallRuleRequest {
92+ ServerUUID : arg ,
93+ Position : rule .Position ,
94+ })
95+ if err != nil {
96+ exec .PushProgressFailed (msg )
97+ if deletedCount > 0 {
98+ exec .PushProgressUpdate (fmt .Sprintf ("Successfully deleted %d rules before error" , deletedCount ))
99+ }
100+ return nil , err
101+ }
102+
103+ exec .PushProgressSuccess (msg )
104+ deletedCount ++
105+
106+ // Adjust positions of remaining rules in our local copy
107+ for i := range server .FirewallRules {
108+ if server .FirewallRules [i ].Position > rule .Position {
109+ server .FirewallRules [i ].Position --
110+ }
111+ }
112+ }
60113
61114 return output.None {}, nil
62115}
116+
117+ func formatRuleAddress (rule * upcloud.FirewallRule , source bool ) string {
118+ var addr , port string
119+ if source {
120+ addr = rule .SourceAddressStart
121+ if rule .SourceAddressEnd != "" && rule .SourceAddressEnd != rule .SourceAddressStart {
122+ addr = fmt .Sprintf ("%s-%s" , addr , rule .SourceAddressEnd )
123+ }
124+ port = rule .SourcePortStart
125+ if rule .SourcePortEnd != "" && rule .SourcePortEnd != rule .SourcePortStart {
126+ port = fmt .Sprintf ("%s-%s" , port , rule .SourcePortEnd )
127+ }
128+ } else {
129+ addr = rule .DestinationAddressStart
130+ if rule .DestinationAddressEnd != "" && rule .DestinationAddressEnd != rule .DestinationAddressStart {
131+ addr = fmt .Sprintf ("%s-%s" , addr , rule .DestinationAddressEnd )
132+ }
133+ port = rule .DestinationPortStart
134+ if rule .DestinationPortEnd != "" && rule .DestinationPortEnd != rule .DestinationPortStart {
135+ port = fmt .Sprintf ("%s-%s" , port , rule .DestinationPortEnd )
136+ }
137+ }
138+
139+ if port != "" {
140+ return fmt .Sprintf ("%s:%s" , addr , port )
141+ }
142+ return addr
143+ }
0 commit comments