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 }