Kaynağa Gözat

Add metabase version override and update (#2370)

* Add version override and update

* Ooppsie

* Quick fix

* fgs copilot

* Allow user to overwrite image, add warning for exposing metabase and general cleanup

* One ix

* Default image if not found in config, and add a warning to remove and update

* Reorder check system memory checks so it inline with @mmetc best pratices

* No need for err

* Clean up some group code

* Change ipv6 as [] seems to wildcard

* Split loopback warn and disclaimer. Add force yes to start to allow user to accept disclaimer by default

* All cmd commands are RunE clean up

* Update flag name and dont allow a shorthand
Laurence Jones 1 yıl önce
ebeveyn
işleme
389ea4293f
3 değiştirilmiş dosya ile 173 ekleme ve 121 silme
  1. 156 108
      cmd/crowdsec-cli/dashboard.go
  2. 5 5
      pkg/metabase/container.go
  3. 12 8
      pkg/metabase/metabase.go

+ 156 - 108
cmd/crowdsec-cli/dashboard.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"errors"
 	"fmt"
 	"math"
 	"os"
@@ -27,6 +26,7 @@ var (
 	metabaseConfigPath   string
 	metabaseConfigFolder = "metabase/"
 	metabaseConfigFile   = "metabase.yaml"
+	metabaseImage        = "metabase/metabase:v0.46.6.1"
 	/**/
 	metabaseListenAddress = "127.0.0.1"
 	metabaseListenPort    = "3000"
@@ -96,7 +96,6 @@ cscli dashboard remove
 	return cmdDashboard
 }
 
-
 func NewDashboardSetupCmd() *cobra.Command {
 	var force bool
 
@@ -111,7 +110,7 @@ cscli dashboard setup
 cscli dashboard setup --listen 0.0.0.0
 cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
  `,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if metabaseDbPath == "" {
 				metabaseDbPath = csConfig.ConfigPaths.DataDir
 			}
@@ -123,70 +122,23 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
 					isValid = passwordIsValid(metabasePassword)
 				}
 			}
-			var answer bool
-			if valid, err := checkSystemMemory(); err == nil && !valid {
-				if !forceYes {
-					prompt := &survey.Confirm{
-						Message: "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?",
-						Default: true,
-					}
-					if err := survey.AskOne(prompt, &answer); err != nil {
-						log.Warnf("unable to ask about RAM check: %s", err)
-					}
-					if !answer {
-						log.Fatal("Unable to continue due to RAM requirement")
-					}
-				} else {
-					log.Warnf("Metabase requires 1-2GB of RAM, your system is below this requirement")
-				}
-			}
-			groupExist := false
-			dockerGroup, err := user.LookupGroup(crowdsecGroup)
-			if err == nil {
-				groupExist = true
-			}
-			if !forceYes && !groupExist {
-				prompt := &survey.Confirm{
-					Message: fmt.Sprintf("For metabase docker to be able to access SQLite file we need to add a new group called '%s' to the system, is it ok for you ?", crowdsecGroup),
-					Default: true,
-				}
-				if err := survey.AskOne(prompt, &answer); err != nil {
-					log.Fatalf("unable to ask to force: %s", err)
-				}
+			if err := checkSystemMemory(&forceYes); err != nil {
+				return err
 			}
-			if !answer && !forceYes && !groupExist {
-				log.Fatalf("unable to continue without creating '%s' group", crowdsecGroup)
+			warnIfNotLoopback(metabaseListenAddress)
+			if err := disclaimer(&forceYes); err != nil {
+				return err
 			}
-			if !groupExist {
-				groupAddCmd, err := exec.LookPath("groupadd")
-				if err != nil {
-					log.Fatalf("unable to find 'groupadd' command, can't continue")
-				}
-
-				groupAdd := &exec.Cmd{Path: groupAddCmd, Args: []string{groupAddCmd, crowdsecGroup}}
-				if err := groupAdd.Run(); err != nil {
-					log.Fatalf("unable to add group '%s': %s", dockerGroup, err)
-				}
-				dockerGroup, err = user.LookupGroup(crowdsecGroup)
-				if err != nil {
-					log.Fatalf("unable to lookup '%s' group: %+v", dockerGroup, err)
-				}
-			}
-			intID, err := strconv.Atoi(dockerGroup.Gid)
+			dockerGroup, err := checkGroups(&forceYes)
 			if err != nil {
-				log.Fatalf("unable to convert group ID to int: %s", err)
+				return err
 			}
-			if err := os.Chown(csConfig.DbConfig.DbPath, 0, intID); err != nil {
-				log.Fatalf("unable to chown sqlite db file '%s': %s", csConfig.DbConfig.DbPath, err)
-			}
-
-			mb, err := metabase.SetupMetabase(csConfig.API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID)
+			mb, err := metabase.SetupMetabase(csConfig.API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
-
 			if err := mb.DumpConfig(metabaseConfigPath); err != nil {
-				log.Fatal(err)
+				return err
 			}
 
 			log.Infof("Metabase is ready")
@@ -194,11 +146,13 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
 			fmt.Printf("\tURL       : '%s'\n", mb.Config.ListenURL)
 			fmt.Printf("\tusername  : '%s'\n", mb.Config.Username)
 			fmt.Printf("\tpassword  : '%s'\n", mb.Config.Password)
+			return nil
 		},
 	}
 	cmdDashSetup.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
 	cmdDashSetup.Flags().StringVarP(&metabaseDbPath, "dir", "d", "", "Shared directory with metabase container")
 	cmdDashSetup.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
+	cmdDashSetup.Flags().StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
 	cmdDashSetup.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
 	cmdDashSetup.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
 	//cmdDashSetup.Flags().StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
@@ -214,18 +168,24 @@ func NewDashboardStartCmd() *cobra.Command {
 		Long:              `Stats the metabase container using docker.`,
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
 			if err != nil {
-				log.Fatal(err)
+				return err
+			}
+			warnIfNotLoopback(mb.Config.ListenAddr)
+			if err := disclaimer(&forceYes); err != nil {
+				return err
 			}
 			if err := mb.Container.Start(); err != nil {
-				log.Fatalf("Failed to start metabase container : %s", err)
+				return fmt.Errorf("failed to start metabase container : %s", err)
 			}
 			log.Infof("Started metabase")
-			log.Infof("url : http://%s:%s", metabaseListenAddress, metabaseListenPort)
+			log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort)
+			return nil
 		},
 	}
+	cmdDashStart.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
 	return cmdDashStart
 }
 
@@ -236,33 +196,33 @@ func NewDashboardStopCmd() *cobra.Command {
 		Long:              `Stops the metabase container using docker.`,
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if err := metabase.StopContainer(metabaseContainerID); err != nil {
-				log.Fatalf("unable to stop container '%s': %s", metabaseContainerID, err)
+				return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
 			}
+			return nil
 		},
 	}
 	return cmdDashStop
 }
 
-
 func NewDashboardShowPasswordCmd() *cobra.Command {
 	var cmdDashShowPassword = &cobra.Command{Use: "show-password",
 		Short:             "displays password of metabase.",
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			m := metabase.Metabase{}
 			if err := m.LoadConfig(metabaseConfigPath); err != nil {
-				log.Fatal(err)
+				return err
 			}
 			log.Printf("'%s'", m.Config.Password)
+			return nil
 		},
 	}
 	return cmdDashShowPassword
 }
 
-
 func NewDashboardRemoveCmd() *cobra.Command {
 	var force bool
 
@@ -276,53 +236,59 @@ func NewDashboardRemoveCmd() *cobra.Command {
 cscli dashboard remove
 cscli dashboard remove --force
  `,
-		Run: func(cmd *cobra.Command, args []string) {
-			answer := true
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if !forceYes {
+				var answer bool
 				prompt := &survey.Confirm{
 					Message: "Do you really want to remove crowdsec dashboard? (all your changes will be lost)",
 					Default: true,
 				}
 				if err := survey.AskOne(prompt, &answer); err != nil {
-					log.Fatalf("unable to ask to force: %s", err)
+					return fmt.Errorf("unable to ask to force: %s", err)
+				}
+				if !answer {
+					return fmt.Errorf("user stated no to continue")
 				}
 			}
-			if answer {
-				if metabase.IsContainerExist(metabaseContainerID) {
-					log.Debugf("Stopping container %s", metabaseContainerID)
-					if err := metabase.StopContainer(metabaseContainerID); err != nil {
-						log.Warningf("unable to stop container '%s': %s", metabaseContainerID, err)
-					}
-					dockerGroup, err := user.LookupGroup(crowdsecGroup)
-					if err == nil { // if group exist, remove it
-						groupDelCmd, err := exec.LookPath("groupdel")
-						if err != nil {
-							log.Fatalf("unable to find 'groupdel' command, can't continue")
-						}
-
-						groupDel := &exec.Cmd{Path: groupDelCmd, Args: []string{groupDelCmd, crowdsecGroup}}
-						if err := groupDel.Run(); err != nil {
-							log.Errorf("unable to delete group '%s': %s", dockerGroup, err)
-						}
+			if metabase.IsContainerExist(metabaseContainerID) {
+				log.Debugf("Stopping container %s", metabaseContainerID)
+				if err := metabase.StopContainer(metabaseContainerID); err != nil {
+					log.Warningf("unable to stop container '%s': %s", metabaseContainerID, err)
+				}
+				dockerGroup, err := user.LookupGroup(crowdsecGroup)
+				if err == nil { // if group exist, remove it
+					groupDelCmd, err := exec.LookPath("groupdel")
+					if err != nil {
+						return fmt.Errorf("unable to find 'groupdel' command, can't continue")
 					}
-					log.Debugf("Removing container %s", metabaseContainerID)
-					if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
-						log.Warningf("unable to remove container '%s': %s", metabaseContainerID, err)
+
+					groupDel := &exec.Cmd{Path: groupDelCmd, Args: []string{groupDelCmd, crowdsecGroup}}
+					if err := groupDel.Run(); err != nil {
+						log.Warnf("unable to delete group '%s': %s", dockerGroup, err)
 					}
-					log.Infof("container %s stopped & removed", metabaseContainerID)
 				}
-				log.Debugf("Removing metabase db %s", csConfig.ConfigPaths.DataDir)
-				if err := metabase.RemoveDatabase(csConfig.ConfigPaths.DataDir); err != nil {
-					log.Warningf("failed to remove metabase internal db : %s", err)
+				log.Debugf("Removing container %s", metabaseContainerID)
+				if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
+					log.Warnf("unable to remove container '%s': %s", metabaseContainerID, err)
+				}
+				log.Infof("container %s stopped & removed", metabaseContainerID)
+			}
+			log.Debugf("Removing metabase db %s", csConfig.ConfigPaths.DataDir)
+			if err := metabase.RemoveDatabase(csConfig.ConfigPaths.DataDir); err != nil {
+				log.Warnf("failed to remove metabase internal db : %s", err)
+			}
+			if force {
+				m := metabase.Metabase{}
+				if err := m.LoadConfig(metabaseConfigPath); err != nil {
+					return err
 				}
-				if force {
-					if err := metabase.RemoveImageContainer(); err != nil {
-						if !strings.Contains(err.Error(), "No such image") {
-							log.Fatalf("removing docker image: %s", err)
-						}
+				if err := metabase.RemoveImageContainer(m.Config.Image); err != nil {
+					if !strings.Contains(err.Error(), "No such image") {
+						return fmt.Errorf("removing docker image: %s", err)
 					}
 				}
 			}
+			return nil
 		},
 	}
 	cmdDashRemove.Flags().BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
@@ -347,13 +313,95 @@ func passwordIsValid(password string) bool {
 
 }
 
-func checkSystemMemory() (bool, error) {
+func checkSystemMemory(forceYes *bool) error {
 	totMem := memory.TotalMemory()
-	if totMem == 0 {
-		return true, errors.New("Unable to get system total memory")
+	if totMem >= uint64(math.Pow(2, 30)) {
+		return nil
+	}
+	if !*forceYes {
+		var answer bool
+		prompt := &survey.Confirm{
+			Message: "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?",
+			Default: true,
+		}
+		if err := survey.AskOne(prompt, &answer); err != nil {
+			return fmt.Errorf("unable to ask about RAM check: %s", err)
+		}
+		if !answer {
+			return fmt.Errorf("user stated no to continue")
+		}
+		return nil
+	}
+	log.Warn("Metabase requires 1-2GB of RAM, your system is below this requirement")
+	return nil
+}
+
+func warnIfNotLoopback(addr string) {
+	if addr == "127.0.0.1" || addr == "::1" {
+		return
+	}
+	log.Warnf("You are potentially exposing your metabase port to the internet (addr: %s), please consider using a reverse proxy", addr)
+}
+
+func disclaimer(forceYes *bool) error {
+	if !*forceYes {
+		var answer bool
+		prompt := &survey.Confirm{
+			Message: "CrowdSec takes no responsibility for the security of your metabase instance. Do you accept these responsibilities ?",
+			Default: true,
+		}
+		if err := survey.AskOne(prompt, &answer); err != nil {
+			return fmt.Errorf("unable to ask to question: %s", err)
+		}
+		if !answer {
+			return fmt.Errorf("user stated no to responsibilities")
+		}
+		return nil
+	}
+	log.Warn("CrowdSec takes no responsibility for the security of your metabase instance. You used force yes, so you accept this disclaimer")
+	return nil
+}
+
+func checkGroups(forceYes *bool) (*user.Group, error) {
+	groupExist := false
+	dockerGroup, err := user.LookupGroup(crowdsecGroup)
+	if err == nil {
+		groupExist = true
+	}
+	if !groupExist {
+		if !*forceYes {
+			var answer bool
+			prompt := &survey.Confirm{
+				Message: fmt.Sprintf("For metabase docker to be able to access SQLite file we need to add a new group called '%s' to the system, is it ok for you ?", crowdsecGroup),
+				Default: true,
+			}
+			if err := survey.AskOne(prompt, &answer); err != nil {
+				return dockerGroup, fmt.Errorf("unable to ask to question: %s", err)
+			}
+			if !answer {
+				return dockerGroup, fmt.Errorf("unable to continue without creating '%s' group", crowdsecGroup)
+			}
+		}
+		groupAddCmd, err := exec.LookPath("groupadd")
+		if err != nil {
+			return dockerGroup, fmt.Errorf("unable to find 'groupadd' command, can't continue")
+		}
+
+		groupAdd := &exec.Cmd{Path: groupAddCmd, Args: []string{groupAddCmd, crowdsecGroup}}
+		if err := groupAdd.Run(); err != nil {
+			return dockerGroup, fmt.Errorf("unable to add group '%s': %s", dockerGroup, err)
+		}
+		dockerGroup, err = user.LookupGroup(crowdsecGroup)
+		if err != nil {
+			return dockerGroup, fmt.Errorf("unable to lookup '%s' group: %+v", dockerGroup, err)
+		}
+	}
+	intID, err := strconv.Atoi(dockerGroup.Gid)
+	if err != nil {
+		return dockerGroup, fmt.Errorf("unable to convert group ID to int: %s", err)
 	}
-	if uint64(math.Pow(2, 30)) >= totMem {
-		return false, nil
+	if err := os.Chown(csConfig.DbConfig.DbPath, 0, intID); err != nil {
+		return dockerGroup, fmt.Errorf("unable to chown sqlite db file '%s': %s", csConfig.DbConfig.DbPath, err)
 	}
-	return true, nil
+	return dockerGroup, nil
 }

+ 5 - 5
pkg/metabase/container.go

@@ -97,7 +97,7 @@ func (c *Container) Create() error {
 	switch os {
 	case "linux":
 	case "windows", "darwin":
-		return fmt.Errorf("Mac and Windows are not supported yet")
+		return fmt.Errorf("mac and windows are not supported yet")
 	default:
 		return fmt.Errorf("OS '%s' is not supported", os)
 	}
@@ -161,15 +161,15 @@ func RemoveContainer(name string) error {
 	return nil
 }
 
-func RemoveImageContainer() error {
+func RemoveImageContainer(image string) error {
 	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	if err != nil {
 		return fmt.Errorf("failed to create docker client : %s", err)
 	}
 	ctx := context.Background()
-	log.Printf("Removing docker image '%s'", metabaseImage)
-	if _, err := cli.ImageRemove(ctx, metabaseImage, types.ImageRemoveOptions{}); err != nil {
-		return fmt.Errorf("failed to remove image container %s : %s", metabaseImage, err)
+	log.Printf("Removing docker image '%s'", image)
+	if _, err := cli.ImageRemove(ctx, image, types.ImageRemoveOptions{}); err != nil {
+		return fmt.Errorf("failed to remove image container %s : %s", image, err)
 	}
 	return nil
 }

+ 12 - 8
pkg/metabase/metabase.go

@@ -38,12 +38,12 @@ type Config struct {
 	Password      string                `yaml:"password"`
 	DBPath        string                `yaml:"metabase_db_path"`
 	DockerGroupID string                `yaml:"-"`
+	Image         string                `yaml:"image"`
 }
 
 var (
 	metabaseDefaultUser     = "crowdsec@crowdsec.net"
 	metabaseDefaultPassword = "!!Cr0wdS3c_M3t4b4s3??"
-	metabaseImage           = "metabase/metabase:v0.41.5"
 	containerSharedFolder   = "/metabase-data"
 	metabaseSQLiteDBURL     = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase_sqlite.zip"
 )
@@ -63,7 +63,7 @@ func TestAvailability() error {
 
 }
 
-func (m *Metabase) Init(containerName string) error {
+func (m *Metabase) Init(containerName string, image string) error {
 	var err error
 	var DBConnectionURI string
 	var remoteDBAddr string
@@ -88,20 +88,19 @@ func (m *Metabase) Init(containerName string) error {
 	if err != nil {
 		return err
 	}
-	m.Container, err = NewContainer(m.Config.ListenAddr, m.Config.ListenPort, m.Config.DBPath, containerName, metabaseImage, DBConnectionURI, m.Config.DockerGroupID)
+	m.Container, err = NewContainer(m.Config.ListenAddr, m.Config.ListenPort, m.Config.DBPath, containerName, image, DBConnectionURI, m.Config.DockerGroupID)
 	if err != nil {
 		return fmt.Errorf("container init: %w", err)
 	}
 
 	return nil
 }
-
 func NewMetabase(configPath string, containerName string) (*Metabase, error) {
 	m := &Metabase{}
 	if err := m.LoadConfig(configPath); err != nil {
 		return m, err
 	}
-	if err := m.Init(containerName); err != nil {
+	if err := m.Init(containerName, m.Config.Image); err != nil {
 		return m, err
 	}
 	return m, nil
@@ -130,14 +129,18 @@ func (m *Metabase) LoadConfig(configPath string) error {
 	if config.ListenURL == "" {
 		return fmt.Errorf("'listen_url' not found in configuration file '%s'", configPath)
 	}
-
+	/* Default image for backporting */
+	if config.Image == "" {
+		config.Image = "metabase/metabase:v0.41.5"
+		log.Warn("Image not found in configuration file, you are using an old dashboard setup (v0.41.5), please remove your dashboard and re-create it to use the latest version.")
+	}
 	m.Config = config
 
 	return nil
 
 }
 
-func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort string, username string, password string, mbDBPath string, dockerGroupID string, containerName string) (*Metabase, error) {
+func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort string, username string, password string, mbDBPath string, dockerGroupID string, containerName string, image string) (*Metabase, error) {
 	metabase := &Metabase{
 		Config: &Config{
 			Database:      dbConfig,
@@ -148,9 +151,10 @@ func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort
 			ListenURL:     fmt.Sprintf("http://%s:%s", listenAddr, listenPort),
 			DBPath:        mbDBPath,
 			DockerGroupID: dockerGroupID,
+			Image:         image,
 		},
 	}
-	if err := metabase.Init(containerName); err != nil {
+	if err := metabase.Init(containerName, image); err != nil {
 		return nil, fmt.Errorf("metabase setup init: %w", err)
 	}