Skip to content

Commit

Permalink
new(cmd): root comand setup
Browse files Browse the repository at this point in the history
Signed-off-by: Leonardo Grasso <me@leonardograsso.com>
  • Loading branch information
leogr committed Apr 7, 2020
1 parent 8c4d956 commit 94f171a
Show file tree
Hide file tree
Showing 8 changed files with 507 additions and 0 deletions.
39 changes: 39 additions & 0 deletions cmd/config_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cmd

import (
"fmt"

"github.com/creasty/defaults"
"github.com/falcosecurity/event-generator/cmd/internal/validate"
"github.com/go-playground/validator/v10"
logger "github.com/sirupsen/logrus"
)

// ConfigOptions represent the persistent configuration flags of event-generator.
type ConfigOptions struct {
ConfigFile string
LogLevel string `validate:"logrus" name:"log level" default:"info"`
}

// NewConfigOptions creates an instance of ConfigOptions.
func NewConfigOptions() *ConfigOptions {
o := &ConfigOptions{}
if err := defaults.Set(o); err != nil {
logger.WithError(err).WithField("options", "ConfigOptions").Fatal("error setting event-generator options defaults")
}
return o
}

// Validate validates the ConfigOptions fields.
func (co *ConfigOptions) Validate() []error {
if err := validate.V.Struct(co); err != nil {
errors := err.(validator.ValidationErrors)
errArr := []error{}
for _, e := range errors {
// Translate each error one at a time
errArr = append(errArr, fmt.Errorf(e.Translate(validate.T)))
}
return errArr
}
return nil
}
28 changes: 28 additions & 0 deletions cmd/internal/validate/isfilepath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package validate

import (
"fmt"
"os"
"reflect"

"github.com/go-playground/validator/v10"
)

func isFilePath(fl validator.FieldLevel) bool {
field := fl.Field()

switch field.Kind() {
case reflect.String:
fileInfo, err := os.Stat(field.String())
if err != nil {
if !os.IsNotExist(err) {
return false
}
return true
}

return !fileInfo.IsDir()
}

panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}
16 changes: 16 additions & 0 deletions cmd/internal/validate/islogruslevel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package validate

import (
"github.com/go-playground/validator/v10"
logger "github.com/sirupsen/logrus"
)

func isLogrusLevel(fl validator.FieldLevel) bool {
level := fl.Field().String()
lvl, err := logger.ParseLevel(level)
if err != nil {
return false
}
logger.SetLevel(lvl)
return true
}
66 changes: 66 additions & 0 deletions cmd/internal/validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package validate

import (
"reflect"
"strings"

"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
)

// V is the validator single instance.
//
// It is a singleton so to cache the structs info.
var V *validator.Validate

// T is the universal translator for validatiors.
var T ut.Translator

func init() {
V = validator.New()

// Register a function to get the field name from "name" tags.
V.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("name"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})

V.RegisterValidation("logrus", isLogrusLevel)
V.RegisterValidation("filepath", isFilePath)

eng := en.New()
uni := ut.New(eng, eng)
T, _ = uni.GetTranslator("en")
en_translations.RegisterDefaultTranslations(V, T)

V.RegisterTranslation(
"filepath",
T,
func(ut ut.Translator) error {
return ut.Add("filepath", "{0} must be a valid file path", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("filepath", fe.Field())

return t
},
)

V.RegisterTranslation(
"logrus",
T,
func(ut ut.Translator) error {
return ut.Add("logrus", "{0} must be a valid logrus level", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("logrus", fe.Field())

return t
},
)
}
152 changes: 152 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package cmd

import (
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

homedir "github.com/mitchellh/go-homedir"
logger "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

func init() {
logger.SetFormatter(&logger.TextFormatter{
ForceColors: true,
DisableLevelTruncation: true,
DisableTimestamp: true,
})
}

// New instantiates the root command.
func New(configOptions *ConfigOptions) *cobra.Command {
if configOptions == nil {
configOptions = NewConfigOptions()
}
rootCmd := &cobra.Command{
Use: "event-generator",
Short: "A command line tool to perform a variety of suspect actions.",
PersistentPreRun: func(c *cobra.Command, args []string) {
// PersistentPreRun runs before flags validation but after args validation.
// Do not assume initialization completed during args validation.

// at this stage configOptions is bound to command line flags only
validateConfig(*configOptions)
initLogger(configOptions.LogLevel)
initConfig(configOptions.ConfigFile)

// then bind all flags to ENV and config file
flags := c.Flags()
initEnv()
initFlags(flags, map[string]bool{
// exclude flags to be not bound to ENV and config file
"config": true,
"loglevel": true,
"help": true,
})
// validateConfig(*configOptions) // enable if other flags were bound to configOptions
debugFlags(flags)
},
Run: func(c *cobra.Command, args []string) {
c.Help()
},
}

flags := rootCmd.PersistentFlags()
flags.StringVarP(&configOptions.ConfigFile, "config", "c", configOptions.ConfigFile, "config file path (default $HOME/.falco-event-generator.yaml if exists)")
flags.StringVarP(&configOptions.LogLevel, "loglevel", "l", configOptions.LogLevel, "log level")

return rootCmd
}

// Execute creates the root command and runs it.
func Execute() {
if err := New(nil).Execute(); err != nil {
logger.WithError(err).Fatal("error executing event-generator")
}
}

// validateConfig
func validateConfig(configOptions ConfigOptions) {
if errs := configOptions.Validate(); errs != nil {
for _, err := range errs {
logger.WithError(err).Error("error validating config options")
}
logger.Fatal("exiting for validation errors")
}
}

// initEnv enables automatic ENV variables lookup
func initEnv() {
viper.AutomaticEnv()
viper.SetEnvPrefix("falco_event_generator")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
}

// initLogger configures the logger
func initLogger(logLevel string) {
lvl, err := logger.ParseLevel(logLevel)
if err != nil {
logger.Fatal(err)
}
logger.SetLevel(lvl)
}

// initConfig reads in config file, if any
func initConfig(configFile string) {
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
logger.WithError(err).Fatal("error getting the home directory")
}

viper.AddConfigPath(home)
viper.SetConfigName(".falco-event-generator")
}

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
logger.WithField("file", viper.ConfigFileUsed()).Info("using config file")
} else {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found, ignore ...
logger.Debug("running without a configuration file")
} else {
// Config file was found but another error was produced
logger.WithField("file", viper.ConfigFileUsed()).WithError(err).Fatal("error running with config file")
}
}
}

// initFlags binds a full flag set to the configuration, using each flag's long name as the config key.
//
// Assuming viper's `AutomaticEnv` is enabled, when a flag is not present in the command line
// will fallback to one of (in order of precedence):
// - ENV (with FALCO_EVENT_GENERATOR prefix)
// - config file (e.g. ~/.falco-event-generator.yaml)
// - its default
func initFlags(flags *pflag.FlagSet, exclude map[string]bool) {
viper.BindPFlags(flags)
flags.VisitAll(func(f *pflag.Flag) {
if exclude[f.Name] {
return
}
if v := viper.GetString(f.Name); v != f.DefValue {
flags.Set(f.Name, v)
}
})
}

func debugFlags(flags *pflag.FlagSet) {
fields := logger.Fields{}
flags.VisitAll(func(f *pflag.Flag) {
if f.Changed {
fields[f.Name] = f.Value
}
})
logger.WithFields(fields).Debug("running with options")
}
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
module github.com/falcosecurity/event-generator

go 1.14

require (
github.com/creasty/defaults v1.3.0
github.com/go-playground/locales v0.13.0
github.com/go-playground/universal-translator v0.17.0
github.com/go-playground/validator/v10 v10.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/sirupsen/logrus v1.5.0
github.com/spf13/cobra v0.0.7
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.6.2
)
Loading

0 comments on commit 94f171a

Please sign in to comment.