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
This commit is contained in:
Laurence Jones 2023-07-25 14:21:25 +01:00 committed by GitHub
parent 77d58652a3
commit 389ea4293f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 172 additions and 120 deletions

View file

@ -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")
}
if err := checkSystemMemory(&forceYes); err != nil {
return err
}
groupExist := false
dockerGroup, err := user.LookupGroup(crowdsecGroup)
if err == nil {
groupExist = true
warnIfNotLoopback(metabaseListenAddress)
if err := disclaimer(&forceYes); err != nil {
return err
}
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 !answer && !forceYes && !groupExist {
log.Fatalf("unable to continue without creating '%s' group", crowdsecGroup)
}
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)
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")
}
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)
}
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.Debugf("Removing container %s", metabaseContainerID)
if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
log.Warningf("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.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)
}
if force {
if err := metabase.RemoveImageContainer(); err != nil {
if !strings.Contains(err.Error(), "No such image") {
log.Fatalf("removing docker image: %s", 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 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 uint64(math.Pow(2, 30)) >= totMem {
return false, 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
}
return true, 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 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 dockerGroup, nil
}

View file

@ -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
}

View file

@ -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)
}