github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/cmd/package-mgmt.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/criteo/command-launcher/internal/command"
    11  	"github.com/criteo/command-launcher/internal/config"
    12  	"github.com/criteo/command-launcher/internal/console"
    13  	"github.com/criteo/command-launcher/internal/context"
    14  	"github.com/criteo/command-launcher/internal/helper"
    15  	"github.com/criteo/command-launcher/internal/pkg"
    16  	"github.com/criteo/command-launcher/internal/remote"
    17  	"github.com/criteo/command-launcher/internal/repository"
    18  	"github.com/spf13/cobra"
    19  	"github.com/spf13/viper"
    20  )
    21  
    22  type PackageFlags struct {
    23  	gitUrl     string
    24  	fileUrl    string
    25  	dropin     bool
    26  	local      bool
    27  	remote     bool
    28  	includeCmd bool
    29  }
    30  
    31  var (
    32  	packageFlags = PackageFlags{}
    33  )
    34  
    35  func AddPackageCmd(rootCmd *cobra.Command, appCtx context.LauncherContext) {
    36  	packageCmd := &cobra.Command{
    37  		Use:   "package",
    38  		Short: "Manage command launcher packages",
    39  		Long:  "Manage command launcher packages",
    40  		RunE: func(cmd *cobra.Command, args []string) error {
    41  			if len(args) == 0 {
    42  				cmd.Help()
    43  			}
    44  			return nil
    45  		},
    46  	}
    47  	packageListCmd := &cobra.Command{
    48  		Use:   "list",
    49  		Short: "List installed packages and commands",
    50  		Long:  "List installed packages and commands with details",
    51  		PreRun: func(cmd *cobra.Command, args []string) {
    52  			if !packageFlags.dropin && !packageFlags.local && !packageFlags.remote {
    53  				packageFlags.dropin = true
    54  				packageFlags.local = true
    55  				packageFlags.remote = false
    56  			}
    57  		},
    58  		Run: func(cmd *cobra.Command, args []string) {
    59  			if packageFlags.local {
    60  				for _, s := range rootCtxt.backend.AllPackageSources() {
    61  					if s.IsManaged && s.Repo != nil {
    62  						printPackages(s.Repo, fmt.Sprintf("managed repository: %s", s.Repo.Name()), packageFlags.includeCmd)
    63  					}
    64  				}
    65  			}
    66  
    67  			if packageFlags.dropin {
    68  				printPackages(rootCtxt.backend.DropinRepository(), "dropin repository", packageFlags.includeCmd)
    69  			}
    70  
    71  			if packageFlags.remote {
    72  				for _, s := range rootCtxt.backend.AllPackageSources() {
    73  					if s.IsManaged {
    74  						remote := remote.CreateRemoteRepository(s.RemoteBaseURL)
    75  						if packages, err := remote.All(); err == nil {
    76  							printPackageInfos(packages, fmt.Sprintf("remote registry: %s", s.Repo.Name()))
    77  						} else {
    78  							console.Warn("Cannot load the remote registry: %v", err)
    79  						}
    80  					}
    81  				}
    82  			}
    83  		},
    84  		ValidArgsFunction: noArgCompletion,
    85  	}
    86  	packageListCmd.Flags().BoolVar(&packageFlags.dropin, "dropin", false, "List only the dropin packages")
    87  	packageListCmd.Flags().BoolVar(&packageFlags.local, "local", false, "List only the local packages")
    88  	packageListCmd.Flags().BoolVar(&packageFlags.remote, "remote", false, "List only the remote packages")
    89  	packageListCmd.Flags().BoolVar(&packageFlags.includeCmd, "include-cmd", false, "List the packages with all commands")
    90  	packageListCmd.Flags().BoolP("all", "a", true, "List all packages")
    91  	packageListCmd.MarkFlagsMutuallyExclusive("all", "dropin", "local", "remote")
    92  
    93  	packageInstallCmd := &cobra.Command{
    94  		Use:   "install [package_name]",
    95  		Short: "Install a dropin package",
    96  		Long:  "Install a dropin package package from a git repo or from a zip file or from its name",
    97  		Args:  cobra.MaximumNArgs(1),
    98  		Example: fmt.Sprintf(`
    99    %s install --git https://example.com/my-repo.git my-pkg`, appCtx.AppName()),
   100  		RunE: func(cmd *cobra.Command, args []string) error {
   101  			if packageFlags.fileUrl != "" {
   102  				return installZipFile(packageFlags.fileUrl)
   103  			}
   104  
   105  			if packageFlags.gitUrl != "" {
   106  				return installGitRepo(packageFlags.gitUrl)
   107  			}
   108  
   109  			return nil
   110  		},
   111  		ValidArgsFunction: noArgCompletion,
   112  	}
   113  	packageInstallCmd.Flags().StringVar(&packageFlags.fileUrl, "file", "", "URL or path of a package file")
   114  	packageInstallCmd.Flags().StringVar(&packageFlags.gitUrl, "git", "", "URL of a Git repo of package")
   115  	packageInstallCmd.MarkFlagsMutuallyExclusive("git", "file")
   116  
   117  	packageDeleteCmd := &cobra.Command{
   118  		Use:   "delete [package_name]",
   119  		Short: "Remove a dropin package",
   120  		Long:  "Remove a dropin package from its name",
   121  		Args:  cobra.ExactArgs(1),
   122  		Example: fmt.Sprintf(`
   123    %s delete my-pkg`, appCtx.AppName()),
   124  		RunE: func(cmd *cobra.Command, args []string) error {
   125  			folder, err := findPackageFolder(args[0])
   126  			if err != nil {
   127  				return err
   128  			}
   129  
   130  			return os.RemoveAll(folder)
   131  		},
   132  		ValidArgsFunction: packageNameValidatonFunc(false, true, false),
   133  	}
   134  
   135  	packageSetupCmd := &cobra.Command{
   136  		Use:   "setup [package_name]",
   137  		Short: "Setup a package",
   138  		Long: `
   139  Manually setup a package.
   140  
   141  This command  will trigger the system command __setup__ defined in the package manifest.
   142  To enable the automatic setup during package installation, enable the configuration:
   143  "enable_package_setup_hook".
   144  `,
   145  		Args: cobra.ExactArgs(1),
   146  		Example: fmt.Sprintf(`
   147    %s setup my-pkg`, appCtx.AppName()),
   148  		RunE: func(cmd *cobra.Command, args []string) error {
   149  			for _, s := range rootCtxt.backend.AllPackageSources() {
   150  				for _, installedPkg := range s.Repo.InstalledPackages() {
   151  					if installedPkg.Name() == args[0] {
   152  						return pkg.ExecSetupHookFromPackage(installedPkg, "")
   153  					}
   154  				}
   155  			}
   156  			return fmt.Errorf("no package named %s found", args[0])
   157  		},
   158  		ValidArgsFunction: packageNameValidatonFunc(true, true, false),
   159  	}
   160  
   161  	packageCmd.AddCommand(packageListCmd)
   162  	packageCmd.AddCommand(packageInstallCmd)
   163  	packageCmd.AddCommand(packageDeleteCmd)
   164  	packageCmd.AddCommand(packageSetupCmd)
   165  	rootCmd.AddCommand(packageCmd)
   166  }
   167  
   168  func packageNameValidatonFunc(includeLocal bool, includeDropin bool, includeRemote bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
   169  	return func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   170  		localPkgs := rootCtxt.backend.DefaultRepository().InstalledPackages()
   171  		dropinPkgs := rootCtxt.backend.DropinRepository().InstalledPackages()
   172  
   173  		pkgTable := map[string]string{}
   174  
   175  		if includeLocal {
   176  			for _, pkg := range localPkgs {
   177  				pkgTable[pkg.Name()] = pkg.Version()
   178  			}
   179  		}
   180  		if includeDropin {
   181  			for _, pkg := range dropinPkgs {
   182  				pkgTable[pkg.Name()] = pkg.Version()
   183  			}
   184  		}
   185  
   186  		if includeRemote {
   187  			remote := remote.CreateRemoteRepository(viper.GetString(config.COMMAND_REPOSITORY_BASE_URL_KEY))
   188  			if packages, err := remote.All(); err == nil {
   189  				for _, pkg := range packages {
   190  					pkgTable[pkg.Name] = pkg.Version
   191  				}
   192  			}
   193  		}
   194  
   195  		availablePkgs := []string{}
   196  		for k, _ := range pkgTable {
   197  			availablePkgs = append(availablePkgs, k)
   198  		}
   199  
   200  		return availablePkgs, cobra.ShellCompDirectiveNoFileComp
   201  	}
   202  }
   203  
   204  func noArgCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   205  	return nil, cobra.ShellCompDirectiveNoFileComp
   206  }
   207  
   208  func printPackages(repo repository.PackageRepository, name string, includeCmd bool) {
   209  	console.Highlight("=== %s ===\n", strings.Title(name))
   210  	for _, pkg := range repo.InstalledPackages() {
   211  		fmt.Printf("  - %-50s %s\n", pkg.Name(), pkg.Version())
   212  		if includeCmd {
   213  			printCommands(pkg.Commands())
   214  		}
   215  	}
   216  	fmt.Println()
   217  }
   218  
   219  func printPackageInfos(packages []remote.PackageInfo, name string) {
   220  	console.Highlight("=== %s ===\n", strings.Title(name))
   221  	for _, pkg := range packages {
   222  		fmt.Printf("%2s %-50s %s\n", "-", pkg.Name, pkg.Version)
   223  	}
   224  	fmt.Println()
   225  }
   226  
   227  func printCommands(commands []command.Command) {
   228  	cmdMap := make(map[string][]command.Command)
   229  	cmdMap["__no_group__"] = make([]command.Command, 0)
   230  
   231  	for _, cmd := range commands {
   232  		if cmd.Type() == "group" {
   233  			cmdMap[cmd.Name()] = make([]command.Command, 0)
   234  		} else if cmd.Type() == "executable" {
   235  			if cmd.Group() != "" {
   236  				cmdMap[cmd.Group()] = append(cmdMap[cmd.Group()], cmd)
   237  			} else {
   238  				cmdMap["__no_group__"] = append(cmdMap[cmd.Group()], cmd)
   239  			}
   240  		}
   241  	}
   242  
   243  	for g, cs := range cmdMap {
   244  		if len(cmdMap[g]) > 0 {
   245  			fmt.Printf("%4s %-49s %s\n", "*", g, "(group)")
   246  			for _, c := range cs {
   247  				fmt.Printf("%6s %-47s %s\n", "-", c.Name(), "(cmd)")
   248  			}
   249  		}
   250  	}
   251  }
   252  
   253  func installGitRepo(gitUrl string) error {
   254  	_, err := url.Parse(gitUrl)
   255  	if err != nil {
   256  		return fmt.Errorf("invalid url or pathname: %v", err)
   257  	}
   258  
   259  	path := viper.GetString(config.DROPIN_FOLDER_KEY)
   260  	gitPkg, err := pkg.CreateGitRepoPackage(gitUrl)
   261  	if err != nil {
   262  		os.RemoveAll(path)
   263  		return fmt.Errorf("failed to install git package %s: %v", gitUrl, err)
   264  	}
   265  
   266  	mf, err := gitPkg.InstallTo(viper.GetString(config.DROPIN_FOLDER_KEY))
   267  	if err != nil {
   268  		os.RemoveAll(path)
   269  		return fmt.Errorf("failed to install git package %s: %v", gitUrl, err)
   270  	}
   271  
   272  	console.Success("Package %s installed in the dropin repository", mf.Name())
   273  	return nil
   274  }
   275  
   276  func installZipFile(fileUrl string) error {
   277  	url, err := url.Parse(fileUrl)
   278  	if err != nil {
   279  		return fmt.Errorf("invalid url or pathname: %v", err)
   280  	}
   281  
   282  	var pathname string
   283  	if url.IsAbs() {
   284  		if url.Scheme == "file" {
   285  			pathname = url.Path
   286  		} else {
   287  			tmpDir, err := os.MkdirTemp("", "package-download-*")
   288  			if err != nil {
   289  				return fmt.Errorf("cannot create temporary dir (%v)", err)
   290  			}
   291  			defer os.RemoveAll(tmpDir)
   292  
   293  			pathname = filepath.Join(tmpDir, filepath.Base(url.Path))
   294  			if err := helper.DownloadFile(fileUrl, pathname, true); err != nil {
   295  				return fmt.Errorf("error downloading %s: %v", url, err)
   296  			}
   297  		}
   298  	} else {
   299  		pathname = url.Path
   300  	}
   301  
   302  	zipPkg, err := pkg.CreateZipPackage(pathname)
   303  	if err != nil {
   304  		return fmt.Errorf("cannot create the package from the zip file: %v", err)
   305  	}
   306  
   307  	targetDir := filepath.Join(viper.GetString(config.DROPIN_FOLDER_KEY), zipPkg.Name())
   308  	if _, err := os.Stat(targetDir); !os.IsNotExist(err) {
   309  		if err := os.RemoveAll(targetDir); err != nil {
   310  			return fmt.Errorf("cannot remove existing package directory %s: %v", targetDir, err)
   311  		}
   312  	}
   313  	if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
   314  		return fmt.Errorf("cannot create target package directory %s: %v", targetDir, err)
   315  	}
   316  
   317  	mf, err := zipPkg.InstallTo(targetDir)
   318  	if err == nil {
   319  		console.Success("Package '%s' version %s installed in the dropin repository", mf.Name(), mf.Version())
   320  	}
   321  
   322  	return err
   323  }
   324  
   325  func findPackageFolder(pkgName string) (string, error) {
   326  	if pkgName == "" {
   327  		return "", fmt.Errorf("invalid package name")
   328  	}
   329  
   330  	var pkgMf command.PackageManifest
   331  	for _, pkg := range rootCtxt.backend.DropinRepository().InstalledPackages() {
   332  		if pkg.Name() == pkgName {
   333  			pkgMf = pkg
   334  			break
   335  		}
   336  	}
   337  
   338  	if pkgMf == nil {
   339  		return "", fmt.Errorf("cannot find the package in the dropin repository")
   340  	}
   341  
   342  	if len(pkgMf.Commands()) == 0 {
   343  		return "", fmt.Errorf("cannot find the package folder in the dropin repository")
   344  	}
   345  
   346  	return pkgMf.Commands()[0].PackageDir(), nil
   347  }