github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/cmd/root.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "github.com/criteo/command-launcher/cmd/metrics" 9 "github.com/criteo/command-launcher/internal/backend" 10 "github.com/criteo/command-launcher/internal/config" 11 ctx "github.com/criteo/command-launcher/internal/context" 12 "github.com/criteo/command-launcher/internal/frontend" 13 "github.com/criteo/command-launcher/internal/repository" 14 "github.com/criteo/command-launcher/internal/updater" 15 "github.com/criteo/command-launcher/internal/user" 16 17 log "github.com/sirupsen/logrus" 18 19 "github.com/spf13/cobra" 20 "github.com/spf13/viper" 21 ) 22 23 const ( 24 EXECUTABLE_NOT_DEFINED = "Executable not defined" 25 ) 26 27 type rootContext struct { 28 appCtx ctx.LauncherContext 29 frontend frontend.Frontend 30 backend backend.Backend 31 32 selfUpdater updater.SelfUpdater 33 cmdUpdaters []*updater.CmdUpdater 34 user user.User 35 metrics metrics.Metrics 36 } 37 38 var ( 39 rootCmd *cobra.Command 40 rootCtxt = rootContext{} 41 ) 42 43 func InitCommands(appName string, appLongName string, version string, buildNum string) { 44 rootCmd = createRootCmd(appName, appLongName) 45 initApp(appName, version, buildNum) 46 } 47 48 func createRootCmd(appName string, appLongName string) *cobra.Command { 49 return &cobra.Command{ 50 Use: appName, 51 Short: fmt.Sprintf("%s - A command launcher π made with <3", appLongName), 52 Long: fmt.Sprintf(` 53 %s - A command launcher π made with <3 54 55 Happy Coding! 56 57 Example: 58 %s --help 59 `, appLongName, appName), 60 PersistentPreRun: preRun, 61 Run: func(cmd *cobra.Command, args []string) { 62 if len(args) == 0 { 63 cmd.Help() 64 } 65 }, 66 PersistentPostRun: postRun, 67 SilenceUsage: true, 68 } 69 } 70 71 func initApp(appName string, appVersion string, buildNum string) { 72 log.SetLevel(log.FatalLevel) 73 rootCtxt.appCtx = ctx.InitContext(appName, appVersion, buildNum) 74 config.LoadConfig(rootCtxt.appCtx) 75 config.InitLog(rootCtxt.appCtx.AppName()) 76 77 rootCtxt.cmdUpdaters = make([]*updater.CmdUpdater, 0) 78 79 initUser() 80 initBackend() 81 82 addBuiltinCommands() 83 initFrontend() 84 } 85 86 // We have to add the ctrl+C 87 func Execute() { 88 if err := rootCmd.Execute(); err != nil { 89 os.Exit(1) 90 } 91 os.Exit(frontend.RootExitCode) 92 } 93 94 func preRun(cmd *cobra.Command, args []string) { 95 if selfUpdateEnabled(cmd, args) { 96 initSelfUpdater() 97 rootCtxt.selfUpdater.CheckUpdateAsync() 98 } 99 100 if cmdUpdateEnabled(cmd, args) { 101 initCmdUpdater() 102 for _, updater := range rootCtxt.cmdUpdaters { 103 updater.CheckUpdateAsync() 104 } 105 } 106 107 graphite := metrics.NewGraphiteMetricsCollector(viper.GetString(config.METRIC_GRAPHITE_HOST_KEY)) 108 extensible := metrics.NewExtensibleMetricsCollector( 109 rootCtxt.backend.SystemCommand(repository.SYSTEM_METRICS_COMMAND), 110 ) 111 rootCtxt.metrics = metrics.NewCompositeMetricsCollector(graphite, extensible) 112 repo, pkg, group, name := cmdAndSubCmd(cmd) 113 rootCtxt.metrics.Collect(rootCtxt.user.Partition, repo, pkg, group, name) 114 } 115 116 func postRun(cmd *cobra.Command, args []string) { 117 if cmdUpdateEnabled(cmd, args) { 118 for _, updater := range rootCtxt.cmdUpdaters { 119 updater.Update() 120 } 121 } 122 123 if selfUpdateEnabled(cmd, args) { 124 rootCtxt.selfUpdater.Update() 125 } 126 127 if metricsEnabled(cmd, args) { 128 err := rootCtxt.metrics.Send(frontend.RootExitCode, cmd.Context().Err()) 129 if err != nil { 130 log.Errorln("Metrics usage βΎοΈ sending has failed") 131 } 132 log.Debug("Successfully send metrics") 133 } 134 } 135 136 func isUpdatePossible(cmd *cobra.Command) bool { 137 cmdPath := cmd.CommandPath() 138 cmdPath = strings.TrimSpace(strings.TrimPrefix(cmdPath, rootCtxt.appCtx.AppName())) 139 // exclude commands for update check 140 // for example version command, you don't want to check new update when requesting current version 141 for _, w := range []string{"version", "config", "completion", "help", "update", "__complete"} { 142 if strings.HasPrefix(cmdPath, w) { 143 return false 144 } 145 } 146 147 return true 148 } 149 150 func selfUpdateEnabled(cmd *cobra.Command, args []string) bool { 151 return viper.GetBool(config.SELF_UPDATE_ENABLED_KEY) && isUpdatePossible(cmd) 152 } 153 154 func cmdUpdateEnabled(cmd *cobra.Command, args []string) bool { 155 return viper.GetBool(config.COMMAND_UPDATE_ENABLED_KEY) && isUpdatePossible(cmd) 156 } 157 158 func metricsEnabled(cmd *cobra.Command, args []string) bool { 159 return viper.GetBool(config.USAGE_METRICS_ENABLED_KEY) && isUpdatePossible(cmd) 160 } 161 162 func initUser() { 163 var err error = nil 164 rootCtxt.user, err = user.GetUser() 165 if err != nil { 166 log.Errorln(err) 167 } 168 log.Infof("User ID: %s User Partition: %d", rootCtxt.user.UID, rootCtxt.user.Partition) 169 } 170 171 func initSelfUpdater() { 172 rootCtxt.selfUpdater = updater.SelfUpdater{ 173 BinaryName: rootCtxt.appCtx.AppName(), 174 LatestVersionUrl: viper.GetString(config.SELF_UPDATE_LATEST_VERSION_URL_KEY), 175 SelfUpdateRootUrl: viper.GetString(config.SELF_UPDATE_BASE_URL_KEY), 176 User: rootCtxt.user, 177 CurrentVersion: rootCtxt.appCtx.AppVersion(), 178 Timeout: viper.GetDuration(config.SELF_UPDATE_TIMEOUT_KEY), 179 } 180 } 181 182 func initCmdUpdater() { 183 for _, source := range rootCtxt.backend.AllPackageSources() { 184 updater := source.InitUpdater( 185 &rootCtxt.user, 186 viper.GetDuration(config.SELF_UPDATE_TIMEOUT_KEY), 187 viper.GetBool(config.CI_ENABLED_KEY), 188 viper.GetString(config.PACKAGE_LOCK_FILE_KEY), 189 viper.GetBool(config.VERIFY_PACKAGE_CHECKSUM_KEY), 190 viper.GetBool(config.VERIFY_PACKAGE_SIGNATURE_KEY), 191 ) 192 if updater != nil { 193 rootCtxt.cmdUpdaters = append(rootCtxt.cmdUpdaters, updater) 194 } 195 } 196 } 197 198 func initBackend() { 199 remotes, _ := config.Remotes() 200 extraSources := []*backend.PackageSource{} 201 for _, remote := range remotes { 202 extraSources = append(extraSources, backend.NewManagedSource( 203 remote.Name, 204 remote.RepositoryDir, 205 remote.RemoteBaseUrl, 206 remote.SyncPolicy, 207 )) 208 } 209 210 rootCtxt.backend, _ = backend.NewDefaultBackend( 211 config.AppDir(), 212 backend.NewDropinSource(viper.GetString(config.DROPIN_FOLDER_KEY)), 213 backend.NewManagedSource( 214 "default", 215 viper.GetString(config.LOCAL_COMMAND_REPOSITORY_DIRNAME_KEY), 216 viper.GetString(config.COMMAND_REPOSITORY_BASE_URL_KEY), 217 backend.SYNC_POLICY_ALWAYS, 218 ), 219 extraSources..., 220 ) 221 222 toBeInitiated := []*backend.PackageSource{} 223 for _, s := range rootCtxt.backend.AllPackageSources() { 224 if s.SyncPolicy != backend.SYNC_POLICY_NEVER && !s.IsInstalled() { 225 toBeInitiated = append(toBeInitiated, s) 226 } 227 } 228 if len(toBeInitiated) > 0 { 229 log.Info("Initialization...") 230 for _, s := range toBeInitiated { 231 s.InitialInstallCommands(&rootCtxt.user, 232 viper.GetBool(config.CI_ENABLED_KEY), 233 viper.GetString(config.PACKAGE_LOCK_FILE_KEY), 234 viper.GetBool(config.VERIFY_PACKAGE_CHECKSUM_KEY), 235 viper.GetBool(config.VERIFY_PACKAGE_SIGNATURE_KEY), 236 ) 237 } 238 rootCtxt.backend.Reload() 239 } 240 } 241 242 func initFrontend() { 243 frontend := frontend.NewDefaultFrontend(rootCtxt.appCtx, rootCmd, rootCtxt.backend) 244 rootCtxt.frontend = frontend 245 246 frontend.AddUserCommands() 247 } 248 249 // return the repo, the package, the group, and the name of the command 250 func cmdAndSubCmd(cmd *cobra.Command) (string, string, string, string) { 251 chain := []string{} 252 253 parent := cmd 254 for parent != nil { 255 //prepend 256 chain = append([]string{parent.Name()}, chain...) 257 parent = parent.Parent() 258 } 259 260 group := "" 261 name := "" 262 263 if len(chain) >= 3 { 264 group, name = chain[1], chain[2] 265 } else if len(chain) == 2 { 266 group, name = "", chain[1] 267 } 268 269 // get the internal command from the group and name 270 iCmd, err := rootCtxt.backend.FindCommand(group, name) 271 if err != nil { 272 return "default", "default", "default", "default" 273 } 274 275 if group == "" { 276 return iCmd.RepositoryID(), iCmd.PackageName(), "default", iCmd.Name() 277 } 278 279 return iCmd.RepositoryID(), iCmd.PackageName(), iCmd.Group(), iCmd.Name() 280 } 281 282 func addBuiltinCommands() { 283 AddVersionCmd(rootCmd, rootCtxt.appCtx) 284 AddConfigCmd(rootCmd, rootCtxt.appCtx) 285 AddLoginCmd(rootCmd, rootCtxt.appCtx, rootCtxt.backend.SystemCommand(repository.SYSTEM_LOGIN_COMMAND)) 286 AddUpdateCmd(rootCmd, rootCtxt.appCtx, rootCtxt.backend.DefaultRepository()) 287 AddCompletionCmd(rootCmd, rootCtxt.appCtx) 288 AddPackageCmd(rootCmd, rootCtxt.appCtx) 289 AddRenameCmd(rootCmd, rootCtxt.appCtx, rootCtxt.backend) 290 AddRemoteCmd(rootCmd, rootCtxt.appCtx, rootCtxt.backend) 291 }