github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/cmd/apps.go (about) 1 package cmd 2 3 // filesCmdGroup represents the instances command 4 import ( 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "text/tabwriter" 10 11 "github.com/cozy/cozy-stack/client" 12 "github.com/cozy/cozy-stack/model/app" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/spf13/cobra" 15 ) 16 17 var ( 18 flagAllDomains bool 19 flagAppsDeactivated bool 20 flagSafeUpdate bool 21 ) 22 23 var ( 24 flagKonnectorAccountID string 25 flagKonnectorsParameters string 26 ) 27 28 var ( 29 flagKonnectorContext string 30 flagKonnectorsShortMaintenance bool 31 flagKonnectorsDisallowManualExec bool 32 ) 33 34 var webappsCmdGroup = &cobra.Command{ 35 Use: "apps <command>", 36 Short: "Interact with the applications", 37 Long: ` 38 cozy-stack apps allows to interact with the cozy applications. 39 40 It provides commands to install or update applications on 41 a cozy. 42 `, 43 RunE: func(cmd *cobra.Command, args []string) error { 44 return cmd.Usage() 45 }, 46 } 47 48 var triggersCmdGroup = &cobra.Command{ 49 Use: "triggers <command>", 50 Short: "Interact with the triggers", 51 Long: ` 52 cozy-stack apps allows to interact with the cozy triggers. 53 54 It provides command to run a specific trigger. 55 `, 56 RunE: func(cmd *cobra.Command, args []string) error { 57 return cmd.Usage() 58 }, 59 } 60 61 var installWebappCmd = &cobra.Command{ 62 Use: "install <slug> [sourceurl]", 63 Short: `Install an application with the specified slug name 64 from the given source URL.`, 65 Example: ` 66 $ cozy-stack apps install --domain cozy.localhost:8080 drive registry://drive/stable 67 $ cozy-stack apps install banks 'git://github.com/cozy/cozy-banks.git#build' 68 $ cozy-stack apps install myapp 'git+ssh://git@gitlab.example.net/team/myapp.git#build' 69 `, 70 Long: "[Some schemes](https://docs.cozy.io/en/cozy-stack/apps/#sources) are allowed as `[sourceurl]`.", 71 RunE: func(cmd *cobra.Command, args []string) error { 72 return installApp(cmd, args, consts.Apps) 73 }, 74 } 75 76 var updateWebappCmd = &cobra.Command{ 77 Use: "update <slug> [sourceurl]", 78 Short: "Update the application with the specified slug name.", 79 Aliases: []string{"upgrade"}, 80 RunE: func(cmd *cobra.Command, args []string) error { 81 return updateApp(cmd, args, consts.Apps) 82 }, 83 } 84 85 var uninstallWebappCmd = &cobra.Command{ 86 Use: "uninstall <slug>", 87 Short: "Uninstall the application with the specified slug name.", 88 Aliases: []string{"rm"}, 89 RunE: func(cmd *cobra.Command, args []string) error { 90 return uninstallApp(cmd, args, consts.Apps) 91 }, 92 } 93 94 var lsWebappsCmd = &cobra.Command{ 95 Use: "ls", 96 Short: "List the installed applications.", 97 RunE: func(cmd *cobra.Command, args []string) error { 98 return lsApps(cmd, args, consts.Apps) 99 }, 100 } 101 102 var showWebappCmd = &cobra.Command{ 103 Use: "show <slug>", 104 Short: "Show the application attributes", 105 RunE: func(cmd *cobra.Command, args []string) error { 106 return showApp(cmd, args, consts.Apps) 107 }, 108 } 109 110 var showWebappTriggersCmd = &cobra.Command{ 111 Use: "show-from-app <slug>", 112 Short: "Show the application triggers", 113 RunE: func(cmd *cobra.Command, args []string) error { 114 return showWebAppTriggers(cmd, args, consts.Apps) 115 }, 116 } 117 118 var showKonnectorCmd = &cobra.Command{ 119 Use: "show <slug>", 120 Short: "Show the application attributes", 121 RunE: func(cmd *cobra.Command, args []string) error { 122 return showApp(cmd, args, consts.Konnectors) 123 }, 124 } 125 126 var konnectorsCmdGroup = &cobra.Command{ 127 Use: "konnectors <command>", 128 Short: "Interact with the konnectors", 129 Long: ` 130 cozy-stack konnectors allows to interact with the cozy konnectors. 131 132 It provides commands to install or update applications on 133 a cozy. 134 `, 135 RunE: func(cmd *cobra.Command, args []string) error { 136 return cmd.Usage() 137 }, 138 } 139 140 var installKonnectorCmd = &cobra.Command{ 141 Use: "install <slug> [sourceurl]", 142 Short: `Install a konnector with the specified slug name 143 from the given source URL.`, 144 Long: ` 145 Install a konnector with the specified slug name. You can also provide the 146 version number to install a specific release if you use the registry:// scheme. 147 Following formats are accepted: 148 registry://<konnector>/<channel>/<version> 149 registry://<konnector>/<channel> 150 registry://<konnector>/<version> 151 registry://<konnector> 152 153 If you provide a channel and a version, the channel is ignored. 154 Default channel is stable. 155 Default version is latest. 156 `, 157 Example: ` 158 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable/1.0.1 159 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable 160 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/1.2.0 161 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline 162 `, 163 RunE: func(cmd *cobra.Command, args []string) error { 164 return installApp(cmd, args, consts.Konnectors) 165 }, 166 } 167 168 var updateKonnectorCmd = &cobra.Command{ 169 Use: "update <slug> [sourceurl]", 170 Short: "Update the konnector with the specified slug name.", 171 Aliases: []string{"upgrade"}, 172 RunE: func(cmd *cobra.Command, args []string) error { 173 return updateApp(cmd, args, consts.Konnectors) 174 }, 175 } 176 177 var uninstallKonnectorCmd = &cobra.Command{ 178 Use: "uninstall <slug>", 179 Short: "Uninstall the konnector with the specified slug name.", 180 Aliases: []string{"rm"}, 181 RunE: func(cmd *cobra.Command, args []string) error { 182 return uninstallApp(cmd, args, consts.Konnectors) 183 }, 184 } 185 186 var lsKonnectorsCmd = &cobra.Command{ 187 Use: "ls", 188 Short: "List the installed konnectors.", 189 RunE: func(cmd *cobra.Command, args []string) error { 190 return lsApps(cmd, args, consts.Konnectors) 191 }, 192 } 193 194 var runKonnectorsCmd = &cobra.Command{ 195 Use: "run <slug>", 196 Short: "Run a konnector.", 197 Long: "Run a konnector named with specified slug using the specified options.", 198 RunE: func(cmd *cobra.Command, args []string) error { 199 if flagDomain == "" { 200 errPrintfln("%s", errMissingDomain) 201 return cmd.Usage() 202 } 203 if len(args) < 1 { 204 return cmd.Usage() 205 } 206 207 slug := args[0] 208 c := newClient(flagDomain, 209 consts.Jobs+":POST:konnector:worker", 210 consts.Triggers, 211 consts.Files, 212 consts.Accounts, 213 ) 214 215 ts, err := c.GetTriggers("konnector") 216 if err != nil { 217 return err 218 } 219 220 type localTrigger struct { 221 id string 222 accountID string 223 } 224 225 var triggers []*localTrigger 226 for _, t := range ts { 227 var msg struct { 228 Slug string `json:"konnector"` 229 Account string `json:"account"` 230 } 231 if err = json.Unmarshal(t.Attrs.Message, &msg); err != nil { 232 return err 233 } 234 if msg.Slug == slug { 235 triggers = append(triggers, &localTrigger{t.ID, msg.Account}) 236 } 237 } 238 239 if len(triggers) == 0 { 240 return fmt.Errorf("Could not find a konnector %q: "+ 241 "it may be installed but it is not activated (no related trigger)", slug) 242 } 243 244 var trigger *localTrigger 245 if len(triggers) > 1 || flagKonnectorAccountID != "" { 246 if flagKonnectorAccountID == "" { 247 return errors.New("Found multiple konnectors with different accounts: use the --account-id flag") 248 } 249 for _, t := range triggers { 250 if t.accountID == flagKonnectorAccountID { 251 trigger = t 252 break 253 } 254 } 255 if trigger == nil { 256 return fmt.Errorf("Could not find konnector linked to account with id %q", 257 flagKonnectorAccountID) 258 } 259 } else { 260 trigger = triggers[0] 261 } 262 263 j, err := c.TriggerLaunch(trigger.id) 264 if err != nil { 265 return err 266 } 267 268 json, err := json.MarshalIndent(j, "", " ") 269 if err != nil { 270 return err 271 } 272 273 fmt.Println(string(json)) 274 return nil 275 }, 276 } 277 278 var listMaintenancesCmd = &cobra.Command{ 279 Use: "ls-maintenances", 280 Short: `List the konnectors in maintenance`, 281 RunE: func(cmd *cobra.Command, args []string) (err error) { 282 ac := newAdminClient() 283 list, err := ac.ListMaintenances(flagKonnectorContext) 284 if err != nil { 285 return err 286 } 287 for _, item := range list { 288 json, err := json.MarshalIndent(item, "", " ") 289 if err != nil { 290 return err 291 } 292 fmt.Println(string(json)) 293 } 294 return nil 295 }, 296 } 297 298 var activateMaintenanceKonnectorsCmd = &cobra.Command{ 299 Use: "maintenance [slug]", 300 Short: `Activate the maintenance for the given konnector`, 301 RunE: func(cmd *cobra.Command, args []string) (err error) { 302 if len(args) != 1 { 303 return cmd.Help() 304 } 305 306 messages := make(map[string]interface{}) 307 for { 308 locale := prompt("Locale (empty to abort):") 309 if locale == "" { 310 break 311 } 312 if len(locale) > 5 { 313 fmt.Fprintf(os.Stdout, "Invalid locale name: %q\n", locale) 314 continue 315 } 316 shortMessage := prompt("Short message:") 317 longMessage := prompt("Long message:") 318 messages[locale] = map[string]string{ 319 "short_message": shortMessage, 320 "long_message": longMessage, 321 } 322 } 323 opts := map[string]interface{}{ 324 "flag_short_maintenance": flagKonnectorsShortMaintenance, 325 "flag_disallow_manual_exec": flagKonnectorsDisallowManualExec, 326 "messages": messages, 327 } 328 ac := newAdminClient() 329 return ac.ActivateMaintenance(args[0], opts) 330 }, 331 } 332 333 var deactivateMaintenanceKonnectorsCmd = &cobra.Command{ 334 Use: "deactivate-maintenance [slug]", 335 Short: `Deactivate maintenance for the given konnector`, 336 RunE: func(cmd *cobra.Command, args []string) (err error) { 337 if len(args) != 1 { 338 return cmd.Help() 339 } 340 ac := newAdminClient() 341 return ac.DeactivateMaintenance(args[0]) 342 }, 343 } 344 345 func installApp(cmd *cobra.Command, args []string, appType string) error { 346 if len(args) < 1 { 347 return cmd.Usage() 348 } 349 slug := args[0] 350 source := "registry://" + slug + "/stable" 351 if len(args) > 1 { 352 source = args[1] 353 } 354 if flagAllDomains { 355 return foreachDomains(func(in *client.Instance) error { 356 c := newClient(in.Attrs.Domain, appType) 357 _, err := c.InstallApp(&client.AppOptions{ 358 AppType: appType, 359 Slug: slug, 360 SourceURL: source, 361 Deactivated: flagAppsDeactivated, 362 }) 363 if err != nil { 364 if err.Error() == app.ErrAlreadyExists.Error() { 365 return nil 366 } 367 return err 368 } 369 fmt.Fprintf(os.Stdout, "Application installed successfully on %s\n", in.Attrs.Domain) 370 return nil 371 }) 372 } 373 if flagDomain == "" { 374 errPrintfln("%s", errMissingDomain) 375 return cmd.Usage() 376 } 377 378 var overridenParameters *json.RawMessage 379 if flagKonnectorsParameters != "" { 380 tmp := json.RawMessage(flagKonnectorsParameters) 381 overridenParameters = &tmp 382 } 383 384 c := newClient(flagDomain, appType) 385 manifest, err := c.InstallApp(&client.AppOptions{ 386 AppType: appType, 387 Slug: slug, 388 SourceURL: source, 389 Deactivated: flagAppsDeactivated, 390 391 OverridenParameters: overridenParameters, 392 }) 393 if err != nil { 394 return err 395 } 396 fmt.Fprintf(os.Stdout, "%s (%s) has been installed on %s\n", slug, manifest.Attrs.Version, flagDomain) 397 398 return nil 399 } 400 401 func updateApp(cmd *cobra.Command, args []string, appType string) error { 402 if len(args) == 0 || len(args) > 2 { 403 return cmd.Usage() 404 } 405 var src string 406 if len(args) > 1 { 407 src = args[1] 408 } 409 if flagAllDomains { 410 return foreachDomains(func(in *client.Instance) error { 411 c := newClient(in.Attrs.Domain, appType) 412 _, err := c.UpdateApp(&client.AppOptions{ 413 AppType: appType, 414 Slug: args[0], 415 SourceURL: src, 416 }, flagSafeUpdate) 417 if err != nil { 418 if err.Error() == app.ErrNotFound.Error() { 419 return nil 420 } 421 return err 422 } 423 fmt.Fprintf(os.Stdout, "Application updated successfully on %s\n", in.Attrs.Domain) 424 return nil 425 }) 426 } 427 if flagDomain == "" { 428 errPrintfln("%s", errMissingDomain) 429 return cmd.Usage() 430 } 431 432 var overridenParameters *json.RawMessage 433 if flagKonnectorsParameters != "" { 434 tmp := json.RawMessage(flagKonnectorsParameters) 435 overridenParameters = &tmp 436 } 437 438 c := newClient(flagDomain, appType) 439 opts := &client.AppOptions{ 440 AppType: appType, 441 Slug: args[0], 442 SourceURL: src, 443 444 OverridenParameters: overridenParameters, 445 } 446 manifest, err := c.GetApp(opts) 447 if err != nil { 448 return err 449 } 450 newManifest, err := c.UpdateApp(opts, flagSafeUpdate) 451 if err != nil { 452 return err 453 } 454 msg := "%s is already up-to-date at %s\n" 455 if app.IsMoreRecent(manifest.Attrs.Version, newManifest.Attrs.Version) { 456 msg = "%s has been upgraded to %s\n" 457 } else if app.IsMoreRecent(newManifest.Attrs.Version, manifest.Attrs.Version) { 458 msg = "%s has been downgraded to %s\n" 459 } 460 fmt.Fprintf(os.Stdout, msg, args[0], newManifest.Attrs.Version) 461 462 return nil 463 } 464 465 func uninstallApp(cmd *cobra.Command, args []string, appType string) error { 466 if len(args) != 1 { 467 return cmd.Usage() 468 } 469 if flagDomain == "" { 470 errPrintfln("%s", errMissingDomain) 471 return cmd.Usage() 472 } 473 c := newClient(flagDomain, appType) 474 manifest, err := c.UninstallApp(&client.AppOptions{ 475 AppType: appType, 476 Slug: args[0], 477 }) 478 if err != nil { 479 return err 480 } 481 fmt.Fprintf(os.Stdout, "%s has been uninstalled\n", manifest.Attrs.Slug) 482 return nil 483 } 484 485 func showApp(cmd *cobra.Command, args []string, appType string) error { 486 if flagDomain == "" { 487 errPrintfln("%s", errMissingDomain) 488 return cmd.Usage() 489 } 490 if len(args) < 1 { 491 return cmd.Usage() 492 } 493 c := newClient(flagDomain, appType) 494 manifest, err := c.GetApp(&client.AppOptions{ 495 Slug: args[0], 496 AppType: appType, 497 }) 498 if err != nil { 499 return err 500 } 501 json, err := json.MarshalIndent(manifest.Attrs, "", " ") 502 if err != nil { 503 return err 504 } 505 fmt.Println(string(json)) 506 return nil 507 } 508 509 func showWebAppTriggers(cmd *cobra.Command, args []string, appType string) error { 510 if flagDomain == "" { 511 errPrintfln("%s", errMissingDomain) 512 return cmd.Usage() 513 } 514 if len(args) < 1 { 515 return cmd.Usage() 516 } 517 c := newClient(flagDomain, appType, consts.Triggers) 518 manifest, err := c.GetApp(&client.AppOptions{ 519 Slug: args[0], 520 AppType: appType, 521 }) 522 if err != nil { 523 return err 524 } 525 526 var triggerIDs []string 527 if manifest.Attrs.Services == nil { 528 fmt.Fprintf(os.Stdout, "No triggers\n") 529 return nil 530 } 531 for _, service := range *manifest.Attrs.Services { 532 if service.TriggerID == "" { 533 continue 534 } 535 triggerIDs = append(triggerIDs, service.TriggerID) 536 } 537 var triggers []*client.Trigger 538 var trigger *client.Trigger 539 for _, triggerID := range triggerIDs { 540 trigger, err = c.GetTrigger(triggerID) 541 if err != nil { 542 return err 543 } 544 545 triggers = append(triggers, trigger) 546 } 547 json, err := json.MarshalIndent(triggers, "", " ") 548 if err != nil { 549 return err 550 } 551 fmt.Println(string(json)) 552 return nil 553 } 554 555 var listTriggerCmd = &cobra.Command{ 556 Use: "ls", 557 Short: `List triggers`, 558 Example: "$ cozy-stack triggers ls --domain cozy.localhost:8080", 559 RunE: func(cmd *cobra.Command, args []string) error { 560 if flagDomain == "" { 561 errPrintfln("%s", errMissingDomain) 562 return cmd.Usage() 563 } 564 c := newClient(flagDomain, consts.Triggers) 565 list, err := c.ListTriggers() 566 if err != nil { 567 return err 568 } 569 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 570 for _, t := range list { 571 fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n", 572 t.ID, 573 t.Attrs.WorkerType, 574 t.Attrs.Type, 575 t.Attrs.Arguments, 576 t.Attrs.Debounce, 577 ) 578 } 579 return w.Flush() 580 }, 581 } 582 583 var launchTriggerCmd = &cobra.Command{ 584 Use: "launch [triggerId]", 585 Short: `Creates a job from a specific trigger`, 586 Example: "$ cozy-stack triggers launch --domain cozy.localhost:8080 748f42b65aca8c99ec2492eb660d1891", 587 RunE: launchTrigger, 588 } 589 590 func launchTrigger(cmd *cobra.Command, args []string) error { 591 if len(args) < 1 { 592 return cmd.Usage() 593 } 594 if flagDomain == "" { 595 errPrintfln("%s", errMissingDomain) 596 return cmd.Usage() 597 } 598 599 // Creates client 600 c := newClient(flagDomain, consts.Triggers) 601 602 // Creates job 603 j, err := c.TriggerLaunch(args[0]) 604 if err != nil { 605 return err 606 } 607 608 // Print JSON 609 json, err := json.MarshalIndent(j, "", " ") 610 if err != nil { 611 return err 612 } 613 fmt.Println(string(json)) 614 return nil 615 } 616 617 func lsApps(cmd *cobra.Command, args []string, appType string) error { 618 if flagDomain == "" { 619 errPrintfln("%s", errMissingDomain) 620 return cmd.Usage() 621 } 622 c := newClient(flagDomain, appType) 623 manifests, err := c.ListApps(appType) 624 if err != nil { 625 return err 626 } 627 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 628 for _, m := range manifests { 629 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", 630 m.Attrs.Slug, 631 m.Attrs.Source, 632 m.Attrs.Version, 633 m.Attrs.State, 634 ) 635 } 636 return w.Flush() 637 } 638 639 func foreachDomains(predicate func(*client.Instance) error) error { 640 ac := newAdminClient() 641 list, err := ac.ListInstances() 642 if err != nil { 643 return nil 644 } 645 var hasErr bool 646 for _, i := range list { 647 if err = predicate(i); err != nil { 648 errPrintfln("%s: %s", i.Attrs.Domain, err) 649 hasErr = true 650 } 651 } 652 if hasErr { 653 return errors.New("At least one error occurred while executing this command") 654 } 655 return nil 656 } 657 658 func init() { 659 webappsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") 660 webappsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively") 661 662 installWebappCmd.PersistentFlags().BoolVar(&flagAppsDeactivated, "ask-permissions", false, "specify that the application should not be activated after installation") 663 updateWebappCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes") 664 updateKonnectorCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes") 665 666 runKonnectorsCmd.PersistentFlags().StringVar(&flagKonnectorAccountID, "account-id", "", "specify the account ID to use for running the konnector") 667 668 listMaintenancesCmd.PersistentFlags().StringVar(&flagKonnectorContext, "context", "", "include konnectors in maintenance for apps registry of this context") 669 activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsShortMaintenance, "short", false, "specify a short maintenance") 670 activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsDisallowManualExec, "no-manual-exec", false, "specify a maintenance disallowing manual execution") 671 672 triggersCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") 673 triggersCmdGroup.AddCommand(launchTriggerCmd) 674 triggersCmdGroup.AddCommand(listTriggerCmd) 675 triggersCmdGroup.AddCommand(showWebappTriggersCmd) 676 677 webappsCmdGroup.AddCommand(lsWebappsCmd) 678 webappsCmdGroup.AddCommand(showWebappCmd) 679 webappsCmdGroup.AddCommand(installWebappCmd) 680 webappsCmdGroup.AddCommand(updateWebappCmd) 681 webappsCmdGroup.AddCommand(uninstallWebappCmd) 682 683 konnectorsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") 684 konnectorsCmdGroup.PersistentFlags().StringVar(&flagKonnectorsParameters, "parameters", "", "override the parameters of the installed konnector") 685 konnectorsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively") 686 687 konnectorsCmdGroup.AddCommand(lsKonnectorsCmd) 688 konnectorsCmdGroup.AddCommand(showKonnectorCmd) 689 konnectorsCmdGroup.AddCommand(installKonnectorCmd) 690 konnectorsCmdGroup.AddCommand(updateKonnectorCmd) 691 konnectorsCmdGroup.AddCommand(uninstallKonnectorCmd) 692 konnectorsCmdGroup.AddCommand(runKonnectorsCmd) 693 konnectorsCmdGroup.AddCommand(listMaintenancesCmd) 694 konnectorsCmdGroup.AddCommand(activateMaintenanceKonnectorsCmd) 695 konnectorsCmdGroup.AddCommand(deactivateMaintenanceKonnectorsCmd) 696 697 RootCmd.AddCommand(triggersCmdGroup) 698 RootCmd.AddCommand(webappsCmdGroup) 699 RootCmd.AddCommand(konnectorsCmdGroup) 700 }