github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/cmd/gosbom/cli/commands.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
     8  	"github.com/gookit/color"
     9  	"github.com/nextlinux/gosbom/cmd/gosbom/cli/options"
    10  	"github.com/nextlinux/gosbom/gosbom"
    11  	"github.com/nextlinux/gosbom/gosbom/event"
    12  	"github.com/nextlinux/gosbom/internal"
    13  	"github.com/nextlinux/gosbom/internal/bus"
    14  	"github.com/nextlinux/gosbom/internal/config"
    15  	"github.com/nextlinux/gosbom/internal/log"
    16  	"github.com/nextlinux/gosbom/internal/version"
    17  	logrusUpstream "github.com/sirupsen/logrus"
    18  	"github.com/spf13/cobra"
    19  	"github.com/spf13/viper"
    20  	"github.com/wagoodman/go-partybus"
    21  
    22  	"github.com/anchore/go-logger/adapter/logrus"
    23  	"github.com/anchore/stereoscope"
    24  )
    25  
    26  const indent = "  "
    27  
    28  // New constructs the `gosbom packages` command, aliases the root command to `gosbom packages`,
    29  // and constructs the `gosbom power-user` command. It is also responsible for
    30  // organizing flag usage and injecting the application config for each command.
    31  // It also constructs the gosbom attest command and the gosbom version command.
    32  
    33  // Because of how the `cobra` library behaves, the application's configuration is initialized
    34  // at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
    35  // Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
    36  // `RunE` is the earliest that the complete application configuration can be loaded.
    37  func New() (*cobra.Command, error) {
    38  	app := &config.Application{}
    39  
    40  	// allow for nested options to be specified via environment variables
    41  	// e.g. pod.context = APPNAME_POD_CONTEXT
    42  	v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")))
    43  
    44  	// since root is aliased as the packages cmd we need to construct this command first
    45  	// we also need the command to have information about the `root` options because of this alias
    46  	ro := &options.RootOptions{}
    47  	po := &options.PackagesOptions{}
    48  	ao := &options.AttestOptions{}
    49  	packagesCmd := Packages(v, app, ro, po)
    50  
    51  	// root options are also passed to the attestCmd so that a user provided config location can be discovered
    52  	poweruserCmd := PowerUser(v, app, ro)
    53  	convertCmd := Convert(v, app, ro, po)
    54  	attestCmd := Attest(v, app, ro, po, ao)
    55  
    56  	// rootCmd is currently an alias for the packages command
    57  	rootCmd := &cobra.Command{
    58  		Use:           fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
    59  		Short:         packagesCmd.Short,
    60  		Long:          packagesCmd.Long,
    61  		Args:          packagesCmd.Args,
    62  		Example:       packagesCmd.Example,
    63  		SilenceUsage:  true,
    64  		SilenceErrors: true,
    65  		RunE:          packagesCmd.RunE,
    66  		Version:       version.FromBuild().Version,
    67  	}
    68  	rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
    69  
    70  	// start adding flags to all the commands
    71  	err := ro.AddFlags(rootCmd, v)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	// package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias
    76  	err = po.AddFlags(rootCmd, v)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	// poweruser also uses the packagesCmd flags since it is a specialized version of the command
    82  	err = po.AddFlags(poweruserCmd, v)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	// commands to add to root
    88  	cmds := []*cobra.Command{
    89  		packagesCmd,
    90  		poweruserCmd,
    91  		convertCmd,
    92  		attestCmd,
    93  		Version(v, app),
    94  		cranecmd.NewCmdAuthLogin("gosbom"), // gosbom login uses the same command as crane
    95  	}
    96  
    97  	// Add sub-commands.
    98  	for _, cmd := range cmds {
    99  		rootCmd.AddCommand(cmd)
   100  	}
   101  
   102  	return rootCmd, err
   103  }
   104  
   105  func validateArgs(cmd *cobra.Command, args []string) error {
   106  	if len(args) == 0 {
   107  		// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
   108  		if err := cmd.Help(); err != nil {
   109  			return fmt.Errorf("unable to display help: %w", err)
   110  		}
   111  		return fmt.Errorf("an image/directory argument is required")
   112  	}
   113  
   114  	return cobra.MaximumNArgs(1)(cmd, args)
   115  }
   116  
   117  func checkForApplicationUpdate() {
   118  	log.Debugf("checking if a new version of %s is available", internal.ApplicationName)
   119  	isAvailable, newVersion, err := version.IsUpdateAvailable()
   120  	if err != nil {
   121  		// this should never stop the application
   122  		log.Errorf(err.Error())
   123  	}
   124  	if isAvailable {
   125  		log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
   126  
   127  		bus.Publish(partybus.Event{
   128  			Type:  event.AppUpdateAvailable,
   129  			Value: newVersion,
   130  		})
   131  	} else {
   132  		log.Debugf("no new %s update available", internal.ApplicationName)
   133  	}
   134  }
   135  
   136  func logApplicationConfig(app *config.Application) {
   137  	versionInfo := version.FromBuild()
   138  	log.Infof("%s version: %+v", internal.ApplicationName, versionInfo.Version)
   139  	log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String()))
   140  }
   141  
   142  func newLogWrapper(app *config.Application) {
   143  	cfg := logrus.Config{
   144  		EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet,
   145  		FileLocation:  app.Log.FileLocation,
   146  		Level:         app.Log.Level,
   147  	}
   148  
   149  	if app.Log.Structured {
   150  		cfg.Formatter = &logrusUpstream.JSONFormatter{
   151  			TimestampFormat:   "2006-01-02 15:04:05",
   152  			DisableTimestamp:  false,
   153  			DisableHTMLEscape: false,
   154  			PrettyPrint:       false,
   155  		}
   156  	}
   157  
   158  	logWrapper, err := logrus.New(cfg)
   159  	if err != nil {
   160  		// this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯
   161  		// I'm going to leave this here in case we one day have a different default logger other than the "discard" logger
   162  		log.Error("unable to initialize logger: %+v", err)
   163  		return
   164  	}
   165  	gosbom.SetLogger(logWrapper)
   166  	stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope"))
   167  }