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 }