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  }