Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(cmd,pkg,docs,docgen,validate): switched from slog go library to rich-text falcoctl log library #333

Merged
merged 5 commits into from
Apr 23, 2024
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
29 changes: 17 additions & 12 deletions cmd/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ package cmd

import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -132,6 +130,8 @@ var tests = []testCase{
"ubuntu-aws",
"--output-module",
"/tmp/falco-ubuntu-aws.ko",
"--output-probe",
"/tmp/falco-ubuntu-aws.o",
"--loglevel",
"debug",
},
Expand All @@ -144,6 +144,7 @@ var tests = []testCase{
env: map[string]string{
"DRIVERKIT_KERNELVERSION": "59",
"DRIVERKIT_OUTPUT_MODULE": "/tmp/falco-ubuntu-aws.ko",
"DRIVERKIT_OUTPUT_PROBE": "/tmp/falco-ubuntu-aws.o",
},
args: []string{
"docker",
Expand Down Expand Up @@ -315,9 +316,14 @@ var tests = []testCase{

func run(t *testing.T, test testCase) {
// Setup
c := NewRootCmd()
b := bytes.NewBufferString("")
c.SetOutput(b)
configOpts, err := NewConfigOptions()
assert.NilError(t, err)
rootOpts, err := NewRootOptions()
assert.NilError(t, err)
var buf bytes.Buffer
configOpts.setOutput(&buf, true)
c := NewRootCmd(configOpts, rootOpts)
c.SetOutput(&buf)
if len(test.args) == 0 || (test.args[0] != "__complete" && test.args[0] != "__completeNoDesc" && test.args[0] != "help" && test.args[0] != "completion") {
test.args = append(test.args, "--dryrun")
}
Expand All @@ -328,19 +334,18 @@ func run(t *testing.T, test testCase) {
}
}
// Test
err := c.Execute()
err = c.Execute()
if err != nil {
if test.expect.err == "" {
t.Fatalf("error executing CLI: %v", err)
} else {
assert.Error(t, err, test.expect.err)
}
// Exactly same behavior as rootCmd.Start(), but here we use ERROR instead of FATAL to avoid leaving
configOpts.Printer.Logger.Error("error executing driverkit", configOpts.Printer.Logger.Args("err", err.Error()))
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error reading CLI output: %v", err)
}
res := stripansi.Strip(string(out))
out := buf.String()
res := stripansi.Strip(out)
assert.Equal(t, test.expect.out, res)
// Teardown
for k := range test.env {
Expand All @@ -365,7 +370,7 @@ type testTemplateData struct {
}

func readTemplateFile(t *testing.T, s string) string {
out, err := ioutil.ReadFile("testdata/templates/" + s)
out, err := os.ReadFile("testdata/templates/" + s)
assert.NilError(t, err)
return string(out)
}
Expand Down
21 changes: 10 additions & 11 deletions cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cmd
import (
"bytes"
"fmt"
"github.com/spf13/pflag"
"os"
"strings"
"text/template"
Expand Down Expand Up @@ -46,12 +47,12 @@ func validateArgs() cobra.PositionalArgs {
if len(args) == 0 {
return nil
}
return cobra.ExactValidArgs(1)(c, args)
return cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)(c, args)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This avoids using a deprecated method.

}
}

// NewCompletionCmd ...
func NewCompletionCmd() *cobra.Command {
func NewCompletionCmd(_ *ConfigOptions, _ *RootOptions, _ *pflag.FlagSet) *cobra.Command {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same signature as other commands.

var long bytes.Buffer
tmpl := template.Must(template.New("long").Parse(longUsageTemplate))
tmpl.Execute(&long, map[string]interface{}{
Expand All @@ -65,25 +66,23 @@ func NewCompletionCmd() *cobra.Command {
Args: validateArgs(),
ValidArgs: cmdArgs,
DisableAutoGenTag: true,
Run: func(c *cobra.Command, args []string) {
RunE: func(c *cobra.Command, args []string) error {
if len(args) == 0 {
c.Help()
return
return c.Help()
}

arg := args[0]
switch arg {
case "bash":
c.Root().GenBashCompletion(os.Stdout)
break
return c.Root().GenBashCompletion(os.Stdout)
case "zsh":
c.Root().GenZshCompletion(os.Stdout)
break
return c.Root().GenZshCompletion(os.Stdout)
case "fish":
c.Root().GenFishCompletion(os.Stdout, true)
return c.Root().GenFishCompletion(os.Stdout, true)
case "help":
c.Help()
return c.Help()
}
return nil
},
}

Expand Down
124 changes: 108 additions & 16 deletions cmd/config_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,143 @@ limitations under the License.
package cmd

import (
"errors"
"fmt"
"log/slog"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
"github.com/mitchellh/go-homedir"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"io"
"os"
"strings"

"github.com/creasty/defaults"
"github.com/falcosecurity/driverkit/validate"
"github.com/go-playground/validator/v10"
"github.com/pterm/pterm"
)

var validProcessors = []string{"docker", "kubernetes", "kubernetes-in-cluster", "local"}
var aliasProcessors = []string{"docker", "k8s", "k8s-ic"}
var configOptions *ConfigOptions

// ConfigOptions represent the persistent configuration flags of driverkit.
type ConfigOptions struct {
ConfigFile string
LogLevel string `validate:"loglevel" name:"log level" default:"INFO"`
configFile string
Timeout int `validate:"number,min=30" default:"120" name:"timeout"`
ProxyURL string `validate:"omitempty,proxy" name:"proxy url"`
DryRun bool
dryRun bool

configErrors bool
// Printer used by all commands to output messages.
Printer *output.Printer
FedeDP marked this conversation as resolved.
Show resolved Hide resolved
// writer is used to write the output of the printer.
writer io.Writer
logLevel *options.LogLevel
disableStyling bool
}

func (co *ConfigOptions) initPrinter() {
// DisableStyling is only enforced by tests.
if co.disableStyling {
pterm.DisableStyling()
}
co.Printer = output.NewPrinter(co.logLevel.ToPtermLogLevel(), pterm.LogFormatterColorful, co.writer)
if co.disableStyling {
// Disable time print for tests
co.Printer.Logger = co.Printer.Logger.WithTime(false)
}

}

// Called by tests to disable styling and set bytes buffer as output
func (co *ConfigOptions) setOutput(writer io.Writer, disableStyling bool) {
co.writer = writer
co.disableStyling = disableStyling
co.initPrinter()
}

// NewConfigOptions creates an instance of ConfigOptions.
func NewConfigOptions() *ConfigOptions {
o := &ConfigOptions{}
func NewConfigOptions() (*ConfigOptions, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When creating a configoptions, default to a INFO level logger.

o := &ConfigOptions{
writer: os.Stdout,
logLevel: options.NewLogLevel(),
disableStyling: false,
}
o.initPrinter()
if err := defaults.Set(o); err != nil {
slog.With("err", err.Error(), "options", "ConfigOptions").Error("error setting driverkit options defaults")
os.Exit(1)
// Return ConfigOptions anyway because we need the logger
return o, err
}
return o
return o, nil
}

// Validate validates the ConfigOptions fields.
func (co *ConfigOptions) Validate() []error {
func (co *ConfigOptions) validate() []error {
if err := validate.V.Struct(co); err != nil {
errors := err.(validator.ValidationErrors)
errArr := []error{}
for _, e := range errors {
var errs validator.ValidationErrors
errors.As(err, &errs)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use errors.As as suggested by ide.
This kind of cleanup is done multiple times.

var errArr []error
for _, e := range errs {
// Translate each error one at a time
errArr = append(errArr, fmt.Errorf(e.Translate(validate.T)))
}
co.configErrors = true
return errArr
}
return nil
}

// AddFlags registers the common flags.
func (co *ConfigOptions) AddFlags(flags *pflag.FlagSet) {
flags.StringVarP(&co.configFile, "config", "c", co.configFile, "config file path (default $HOME/.driverkit.yaml if exists)")
flags.VarP(co.logLevel, "loglevel", "l", "set level for logs "+co.logLevel.Allowed())
flags.IntVar(&co.Timeout, "timeout", co.Timeout, "timeout in seconds")
flags.StringVar(&co.ProxyURL, "proxy", co.ProxyURL, "the proxy to use to download data")
flags.BoolVar(&co.dryRun, "dryrun", co.dryRun, "do not actually perform the action")
}

// Init reads in config file and ENV variables if set.
func (co *ConfigOptions) Init() bool {
configErr := false
if errs := co.validate(); errs != nil {
for _, err := range errs {
co.Printer.Logger.Error("error validating config options",
co.Printer.Logger.Args("err", err.Error()))
}
configErr = true
}
if co.configFile != "" {
viper.SetConfigFile(co.configFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
co.Printer.Logger.Error("error getting the home directory",
co.Printer.Logger.Args("err", err.Error()))
// not setting configErr = true because we fallback to `$HOME/.driverkit.yaml` and try with it
}

viper.AddConfigPath(home)
viper.SetConfigName(".driverkit")
}

viper.AutomaticEnv()
viper.SetEnvPrefix("driverkit")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

// If a config file is found, read it in.
err := viper.ReadInConfig()
// Init printer with either read or existent one,
// so that we can further log considering log level set.
co.initPrinter()
if err == nil {
co.Printer.Logger.Info("using config file",
co.Printer.Logger.Args("file", viper.ConfigFileUsed()))
} else {
var configFileNotFoundError viper.ConfigFileNotFoundError
if errors.As(err, &configFileNotFoundError) {
// Config file not found, ignore ...
co.Printer.Logger.Debug("running without a configuration file")
}
}
return configErr
}
38 changes: 24 additions & 14 deletions cmd/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,42 @@ limitations under the License.
package cmd

import (
"log/slog"
"os"

"bytes"
"github.com/falcosecurity/driverkit/pkg/driverbuilder"
"github.com/falcosecurity/driverkit/pkg/driverbuilder/builder"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

// NewDockerCmd creates the `driverkit docker` command.
func NewDockerCmd(rootOpts *RootOptions, rootFlags *pflag.FlagSet) *cobra.Command {
func NewDockerCmd(configOpts *ConfigOptions, rootOpts *RootOptions, rootFlags *pflag.FlagSet) *cobra.Command {
dockerCmd := &cobra.Command{
Use: "docker",
Short: "Build Falco kernel modules and eBPF probes against a docker daemon.",
Run: func(c *cobra.Command, args []string) {
slog.With("processor", c.Name()).Info("driver building, it will take a few seconds")
if !configOptions.DryRun {
b := rootOpts.ToBuild()
if !b.HasOutputs() {
return
RunE: func(c *cobra.Command, args []string) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another small change: return an error from executors instead of dealing with os.Exit(1) ourselves.

configOpts.Printer.Logger.Info("starting build",
configOpts.Printer.Logger.Args("processor", c.Name()))
if !configOpts.dryRun {
if !rootOpts.Output.HasOutputs() {
configOpts.Printer.Logger.Info("no output specified")
return nil
}
if err := driverbuilder.NewDockerBuildProcessor(viper.GetInt("timeout"), viper.GetString("proxy")).Start(b); err != nil {
slog.With("err", err.Error()).Error("exiting")
os.Exit(1)
// Since we use a spinner, cache log data to a bytesbuffer;
// we will later print it once we stop the spinner.
var b *builder.Build
if configOpts.disableStyling {
b = rootOpts.ToBuild(configOpts.Printer)
} else {
var buf bytes.Buffer
b = rootOpts.ToBuild(configOpts.Printer.WithWriter(&buf))
configOpts.Printer.Spinner, _ = configOpts.Printer.Spinner.Start("driver building, it will take a few seconds")
defer func() {
configOpts.Printer.DefaultText.Print(buf.String())
}()
}
return driverbuilder.NewDockerBuildProcessor(configOpts.Timeout, configOpts.ProxyURL).Start(b)
}
return nil
},
}
// Add root flags
Expand Down
Loading
Loading