diff --git a/cmd/crowdsec-cli/bouncers.go b/cmd/crowdsec-cli/bouncers.go index e5ad5973b..3c92d22b8 100644 --- a/cmd/crowdsec-cli/bouncers.go +++ b/cmd/crowdsec-cli/bouncers.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -60,8 +61,7 @@ func getBouncers(out io.Writer, dbClient *database.Client) error { func NewBouncersListCmd() *cobra.Command { cmdBouncersList := &cobra.Command{ Use: "list", - Short: "List bouncers", - Long: `List bouncers`, + Short: "list all bouncers within the database", Example: `cscli bouncers list`, Args: cobra.ExactArgs(0), DisableAutoGenTag: true, @@ -128,8 +128,7 @@ func runBouncersAdd(cmd *cobra.Command, args []string) error { func NewBouncersAddCmd() *cobra.Command { cmdBouncersAdd := &cobra.Command{ Use: "add MyBouncerName [--length 16]", - Short: "add bouncer", - Long: `add bouncer`, + Short: "add a single bouncer to the database", Example: `cscli bouncers add MyBouncerName cscli bouncers add MyBouncerName -l 24 cscli bouncers add MyBouncerName -k `, @@ -161,7 +160,7 @@ func runBouncersDelete(cmd *cobra.Command, args []string) error { func NewBouncersDeleteCmd() *cobra.Command { cmdBouncersDelete := &cobra.Command{ Use: "delete MyBouncerName", - Short: "delete bouncer", + Short: "delete a single bouncer from the database", Args: cobra.MinimumNArgs(1), Aliases: []string{"remove"}, DisableAutoGenTag: true, @@ -190,11 +189,81 @@ func NewBouncersDeleteCmd() *cobra.Command { return cmdBouncersDelete } +func NewBouncersPruneCmd() *cobra.Command { + var parsedDuration time.Duration + cmdBouncersPrune := &cobra.Command{ + Use: "prune", + Short: "prune multiple bouncers from the database", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + Example: `cscli bouncers prune -d 60m +cscli bouncers prune -d 60m --force`, + PreRunE: func(cmd *cobra.Command, args []string) error { + dur, _ := cmd.Flags().GetString("duration") + var err error + parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur)) + if err != nil { + return fmt.Errorf("unable to parse duration '%s': %s", dur, err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + force, _ := cmd.Flags().GetBool("force") + if parsedDuration >= 0-2*time.Minute { + var answer bool + prompt := &survey.Confirm{ + Message: "The duration you provided is less than or equal 2 minutes this may remove active bouncers continue ?", + Default: false, + } + if err := survey.AskOne(prompt, &answer); err != nil { + return fmt.Errorf("unable to ask about prune check: %s", err) + } + if !answer { + fmt.Println("user aborted prune no changes were made") + return nil + } + } + bouncers, err := dbClient.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(parsedDuration)) + if err != nil { + return fmt.Errorf("unable to query bouncers: %s", err) + } + if len(bouncers) == 0 { + fmt.Println("no bouncers to prune") + return nil + } + getBouncersTable(color.Output, bouncers) + if !force { + var answer bool + prompt := &survey.Confirm{ + Message: "You are about to PERMANENTLY remove the above bouncers from the database these will NOT be recoverable, continue ?", + Default: false, + } + if err := survey.AskOne(prompt, &answer); err != nil { + return fmt.Errorf("unable to ask about prune check: %s", err) + } + if !answer { + fmt.Println("user aborted prune no changes were made") + return nil + } + } + nbDeleted, err := dbClient.BulkDeleteBouncers(bouncers) + if err != nil { + return fmt.Errorf("unable to prune bouncers: %s", err) + } + fmt.Printf("successfully delete %d bouncers\n", nbDeleted) + return nil + }, + } + cmdBouncersPrune.Flags().StringP("duration", "d", "60m", "duration of time since last pull") + cmdBouncersPrune.Flags().Bool("force", false, "force prune without asking for confirmation") + return cmdBouncersPrune +} + func NewBouncersCmd() *cobra.Command { var cmdBouncers = &cobra.Command{ Use: "bouncers [action]", Short: "Manage bouncers [requires local API]", - Long: `To list/add/delete bouncers. + Long: `To list/add/delete/prune bouncers. Note: This command requires database direct access, so is intended to be run on Local API/master. `, Args: cobra.MinimumNArgs(1), @@ -217,6 +286,7 @@ Note: This command requires database direct access, so is intended to be run on cmdBouncers.AddCommand(NewBouncersListCmd()) cmdBouncers.AddCommand(NewBouncersAddCmd()) cmdBouncers.AddCommand(NewBouncersDeleteCmd()) + cmdBouncers.AddCommand(NewBouncersPruneCmd()) return cmdBouncers } diff --git a/pkg/database/bouncers.go b/pkg/database/bouncers.go index 98bfd4587..804337ecc 100644 --- a/pkg/database/bouncers.go +++ b/pkg/database/bouncers.go @@ -69,6 +69,18 @@ func (c *Client) DeleteBouncer(name string) error { return nil } +func (c *Client) BulkDeleteBouncers(bouncers []*ent.Bouncer) (int, error) { + ids := make([]int, len(bouncers)) + for i, b := range bouncers { + ids[i] = b.ID + } + nbDeleted, err := c.Ent.Bouncer.Delete().Where(bouncer.IDIn(ids...)).Exec(c.CTX) + if err != nil { + return nbDeleted, fmt.Errorf("unable to delete bouncers: %s", err) + } + return nbDeleted, nil +} + func (c *Client) UpdateBouncerLastPull(lastPull time.Time, ID int) error { _, err := c.Ent.Bouncer.UpdateOneID(ID). SetLastPull(lastPull). @@ -94,3 +106,7 @@ func (c *Client) UpdateBouncerTypeAndVersion(bType string, version string, ID in } return nil } + +func (c *Client) QueryBouncersLastPulltimeLT(t time.Time) ([]*ent.Bouncer, error) { + return c.Ent.Bouncer.Query().Where(bouncer.LastPullLT(t)).All(c.CTX) +}