Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ var (
&databaseInfoCommand,
&databaseCreateCommand,
&databaseDestroyCommand,

// Firewall rules
&databaseFirewallRulesListCommand,
&databaseFirewallRulesAddCommand,
&databaseFirewallRulesRemoveCommand,
&databaseFirewallManagedRangesCommand,
}

globalCommands = []*cli.Command{
Expand Down
229 changes: 229 additions & 0 deletions cmd/firewall_rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package cmd

import (
"context"
"errors"

"github.com/urfave/cli/v3"

"github.com/Scalingo/cli/cmd/autocomplete"
"github.com/Scalingo/cli/dbng"
"github.com/Scalingo/cli/detect"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-scalingo/v8"
)

var (
databaseFirewallRulesListCommand = cli.Command{
Name: "database-firewall-rules",
Category: "Databases NG",
Usage: "List firewall rules of a database",
ArgsUsage: "database-id",
Flags: []cli.Flag{databaseFlag()},
Description: CommandDescription{
Description: "List all firewall rules of a database next generation",
Examples: []string{
"scalingo database-firewall-rules my-db-id",
"scalingo --database my-db database-firewall-rules",
},
SeeAlso: []string{"database-firewall-rules-add", "database-firewall-rules-remove", "database-firewall-managed-ranges"},
}.Render(),
Action: func(ctx context.Context, c *cli.Command) error {
databaseName, addonID, err := detect.GetDatabaseFromArgs(ctx, c)
if errors.Is(err, detect.ErrTooManyArguments) {
io.Error(err)
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules")
}
if err != nil {
errorQuit(ctx, err)
}

Comment on lines +38 to +41

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Could be simplified by:

Suggested change
if err != nil {
if err == detect.ErrTooManyArguments {
io.Error(err)
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules")
}
errorQuit(ctx, err)
}
if errors.Is(err, detect.ErrTooManyArguments) {
io.Error(err)
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules")
}
if err != nil {
errorQuit(ctx, err)
}

In addition, this is a good practice to use errors.Is instead of direct comparison.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much cleaner indeed. Thanks.

utils.CheckForConsent(ctx, databaseName, utils.ConsentTypeDBs)

err = dbng.FirewallRulesList(ctx, databaseName, addonID)
if err != nil {
errorQuit(ctx, err)
}

return nil
},
ShellComplete: func(ctx context.Context, c *cli.Command) {
_ = autocomplete.CmdFlagsAutoComplete(c, "database-firewall-rules")
_ = autocomplete.DatabasesNgListAutoComplete(ctx)
},
}

databaseFirewallRulesAddCommand = cli.Command{
Name: "database-firewall-rules-add",
Category: "Databases NG",
Usage: "Add a firewall rule to a database",
ArgsUsage: "database-id",
Flags: []cli.Flag{
databaseFlag(),
&cli.StringFlag{Name: "cidr", Usage: "CIDR range (e.g., 203.0.113.0/24)"},
&cli.StringFlag{Name: "label", Usage: "Label for the custom rule"},
&cli.StringFlag{Name: "managed-range", Usage: "Managed range ID"},
},
Description: CommandDescription{
Description: "Add a firewall rule to a database next generation. Either --cidr or --managed-range must be specified, but not both.",
Examples: []string{
"scalingo database-firewall-rules-add my-db-id --cidr 203.0.113.0/24 --label \"Office network\"",
"scalingo --database my-db database-firewall-rules-add --managed-range mr-scalingo-osc-fr1",
},
SeeAlso: []string{"database-firewall-rules", "database-firewall-rules-remove", "database-firewall-managed-ranges"},
}.Render(),
Action: func(ctx context.Context, c *cli.Command) error {
databaseName, addonID, err := detect.GetDatabaseFromArgs(ctx, c)
if errors.Is(err, detect.ErrTooManyArguments) {
io.Error(err)
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-add")
}
if err != nil {
errorQuit(ctx, err)
Comment on lines +82 to +83

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

}

cidr := c.String("cidr")
label := c.String("label")
managedRange := c.String("managed-range")

if cidr == "" && managedRange == "" {
io.Error("Either --cidr or --managed-range must be specified")
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-add")
}

if cidr != "" && managedRange != "" {
io.Error("Cannot specify both --cidr and --managed-range")
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-add")
}

utils.CheckForConsent(ctx, databaseName, utils.ConsentTypeDBs)

var params scalingo.FirewallRuleCreateParams
if cidr != "" {
params = scalingo.FirewallRuleCreateParams{
Type: scalingo.FirewallRuleTypeCustomRange,
CIDR: cidr,
Label: label,
}
} else {
params = scalingo.FirewallRuleCreateParams{
Type: scalingo.FirewallRuleTypeManagedRange,
RangeID: managedRange,
Comment thread
matthieu526-scalingo marked this conversation as resolved.
Resolved
Label: label,
}
}

err = dbng.FirewallRulesAdd(ctx, databaseName, addonID, params)
if err != nil {
errorQuit(ctx, err)
}

return nil
},
ShellComplete: func(ctx context.Context, c *cli.Command) {
_ = autocomplete.CmdFlagsAutoComplete(c, "database-firewall-rules-add")
_ = autocomplete.DatabasesNgListAutoComplete(ctx)
},
}

databaseFirewallRulesRemoveCommand = cli.Command{
Name: "database-firewall-rules-remove",
Category: "Databases NG",
Usage: "Remove a firewall rule from a database",
ArgsUsage: "database-id rule-id",
Flags: []cli.Flag{databaseFlag()},
Description: CommandDescription{
Description: "Remove a firewall rule from a database next generation",
Examples: []string{
"scalingo database-firewall-rules-remove my-db-id rule-id",
"scalingo --database my-db database-firewall-rules-remove rule-id",
},
SeeAlso: []string{"database-firewall-rules", "database-firewall-rules-add", "database-firewall-managed-ranges"},
}.Render(),
Action: func(ctx context.Context, c *cli.Command) error {
var databaseName, addonID, ruleID string
var err error

args := c.Args().Slice()
switch len(args) {
case 0:
// No arguments provided
io.Error("Missing required argument: rule-id")
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-remove")
case 1:
// Only rule-id provided, database from --database flag
databaseName, addonID = detect.GetCurrentDatabase(ctx, c)
ruleID = args[0]
case 2:
// Both database-name and rule-id provided as positional args
databaseName = args[0]
ruleID = args[1]
addonID, err = detect.GetAddonIDFromDatabase(ctx, databaseName)
if err != nil {
errorQuit(ctx, err)
}
default:
io.Error("Invalid number of arguments")
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-remove")
}

if databaseName == "" {
io.Error("Please provide a database name or use --database flag")
return cli.ShowCommandHelp(ctx, c, "database-firewall-rules-remove")
}

utils.CheckForConsent(ctx, databaseName, utils.ConsentTypeDBs)

err = dbng.FirewallRulesRemove(ctx, databaseName, addonID, ruleID)
if err != nil {
errorQuit(ctx, err)
}

return nil
},
ShellComplete: func(ctx context.Context, c *cli.Command) {
_ = autocomplete.CmdFlagsAutoComplete(c, "database-firewall-rules-remove")
_ = autocomplete.DatabasesNgListAutoComplete(ctx)
},
}

databaseFirewallManagedRangesCommand = cli.Command{
Name: "database-firewall-managed-ranges",
Category: "Databases NG",
Usage: "List available managed ranges for a database",
ArgsUsage: "database-id",
Flags: []cli.Flag{databaseFlag()},
Description: CommandDescription{
Description: "List all available managed ranges for firewall rules of a database next generation",
Examples: []string{
"scalingo database-firewall-managed-ranges my-db-id",
"scalingo --database my-db database-firewall-managed-ranges",
},
SeeAlso: []string{"database-firewall-rules", "database-firewall-rules-add", "database-firewall-rules-remove"},
}.Render(),
Action: func(ctx context.Context, c *cli.Command) error {
databaseName, addonID, err := detect.GetDatabaseFromArgs(ctx, c)
if errors.Is(err, detect.ErrTooManyArguments) {
io.Error(err)
return cli.ShowCommandHelp(ctx, c, "database-firewall-managed-ranges")
}
if err != nil {
errorQuit(ctx, err)
}
Comment on lines +211 to 213

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above


utils.CheckForConsent(ctx, databaseName, utils.ConsentTypeDBs)

err = dbng.FirewallManagedRangesList(ctx, databaseName, addonID)
if err != nil {
errorQuit(ctx, err)
}

return nil
},
ShellComplete: func(ctx context.Context, c *cli.Command) {
_ = autocomplete.CmdFlagsAutoComplete(c, "database-firewall-managed-ranges")
_ = autocomplete.DatabasesNgListAutoComplete(ctx)
},
}
)
114 changes: 114 additions & 0 deletions dbng/firewall_rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package dbng

import (
"context"
"os"

"github.com/olekukonko/tablewriter"

"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/go-scalingo/v8"
"github.com/Scalingo/go-utils/errors/v2"
Comment thread
sc-david-voisin marked this conversation as resolved.
Resolved
)

func FirewallRulesList(ctx context.Context, databaseID, addonID string) error {
c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
}

rules, err := c.Preview().FirewallRulesList(ctx, databaseID, addonID)
if err != nil {
return errors.Wrap(ctx, err, "list firewall rules")
}

if len(rules) == 0 {
io.Status("No firewall rules configured for this database.")
return nil
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"ID", "Type", "CIDR or Range ID", "Label"})

for _, rule := range rules {
cidrOrRange := rule.CIDR
if rule.Type == scalingo.FirewallRuleTypeManagedRange {
cidrOrRange = rule.RangeID
}

_ = t.Append([]string{
rule.ID,
string(rule.Type),
cidrOrRange,
rule.Label,
})
Comment thread
briceamen marked this conversation as resolved.
Resolved
}
_ = t.Render()

return nil
}

func FirewallRulesAdd(ctx context.Context, databaseID, addonID string, params scalingo.FirewallRuleCreateParams) error {
c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
}

rule, err := c.Preview().FirewallRulesCreate(ctx, databaseID, addonID, params)
if err != nil {
return errors.Wrap(ctx, err, "add firewall rule")
}

io.Statusf("Firewall rule '%s' has been added to database '%s'.\n", rule.ID, databaseID)
io.Warning("Expect some delay for the firewall rules to be applied.")

return nil
}

func FirewallRulesRemove(ctx context.Context, databaseID, addonID, ruleID string) error {
c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
}

err = c.Preview().FirewallRulesDestroy(ctx, databaseID, addonID, ruleID)
if err != nil {
return errors.Wrap(ctx, err, "remove firewall rule")
}

io.Statusf("Firewall rule '%s' has been removed from database '%s'.\n", ruleID, databaseID)
io.Warning("Expect some delay for the firewall rules to be applied.")

return nil
}

func FirewallManagedRangesList(ctx context.Context, databaseID, addonID string) error {
c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
}

ranges, err := c.Preview().FirewallRulesGetManagedRanges(ctx, databaseID, addonID)
if err != nil {
return errors.Wrap(ctx, err, "list managed ranges")
}

if len(ranges) == 0 {
io.Status("No managed ranges available.")
return nil
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"ID", "Name"})

for _, r := range ranges {
_ = t.Append([]string{
r.ID,
r.Name,
})
}
_ = t.Render()

return nil
}
Loading