package cmd // filesCmdGroup represents the instances command import ( "encoding/json" "errors" "fmt" "os" "text/tabwriter" "github.com/cozy/cozy-stack/client" "github.com/cozy/cozy-stack/model/app" "github.com/cozy/cozy-stack/pkg/consts" "github.com/spf13/cobra" ) var ( flagAllDomains bool flagAppsDeactivated bool flagSafeUpdate bool ) var ( flagKonnectorAccountID string flagKonnectorsParameters string ) var ( flagKonnectorContext string flagKonnectorsShortMaintenance bool flagKonnectorsDisallowManualExec bool ) var webappsCmdGroup = &cobra.Command{ Use: "apps ", Short: "Interact with the applications", Long: ` cozy-stack apps allows to interact with the cozy applications. It provides commands to install or update applications on a cozy. `, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } var triggersCmdGroup = &cobra.Command{ Use: "triggers ", Short: "Interact with the triggers", Long: ` cozy-stack apps allows to interact with the cozy triggers. It provides command to run a specific trigger. `, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } var installWebappCmd = &cobra.Command{ Use: "install [sourceurl]", Short: `Install an application with the specified slug name from the given source URL.`, Example: ` $ cozy-stack apps install --domain cozy.localhost:8080 drive registry://drive/stable $ cozy-stack apps install banks 'git://github.com/cozy/cozy-banks.git#build' $ cozy-stack apps install myapp 'git+ssh://git@gitlab.example.net/team/myapp.git#build' `, Long: "[Some schemes](https://docs.cozy.io/en/cozy-stack/apps/#sources) are allowed as `[sourceurl]`.", RunE: func(cmd *cobra.Command, args []string) error { return installApp(cmd, args, consts.Apps) }, } var updateWebappCmd = &cobra.Command{ Use: "update [sourceurl]", Short: "Update the application with the specified slug name.", Aliases: []string{"upgrade"}, RunE: func(cmd *cobra.Command, args []string) error { return updateApp(cmd, args, consts.Apps) }, } var uninstallWebappCmd = &cobra.Command{ Use: "uninstall ", Short: "Uninstall the application with the specified slug name.", Aliases: []string{"rm"}, RunE: func(cmd *cobra.Command, args []string) error { return uninstallApp(cmd, args, consts.Apps) }, } var lsWebappsCmd = &cobra.Command{ Use: "ls", Short: "List the installed applications.", RunE: func(cmd *cobra.Command, args []string) error { return lsApps(cmd, args, consts.Apps) }, } var showWebappCmd = &cobra.Command{ Use: "show ", Short: "Show the application attributes", RunE: func(cmd *cobra.Command, args []string) error { return showApp(cmd, args, consts.Apps) }, } var showWebappTriggersCmd = &cobra.Command{ Use: "show-from-app ", Short: "Show the application triggers", RunE: func(cmd *cobra.Command, args []string) error { return showWebAppTriggers(cmd, args, consts.Apps) }, } var showKonnectorCmd = &cobra.Command{ Use: "show ", Short: "Show the application attributes", RunE: func(cmd *cobra.Command, args []string) error { return showApp(cmd, args, consts.Konnectors) }, } var konnectorsCmdGroup = &cobra.Command{ Use: "konnectors ", Short: "Interact with the konnectors", Long: ` cozy-stack konnectors allows to interact with the cozy konnectors. It provides commands to install or update applications on a cozy. `, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } var installKonnectorCmd = &cobra.Command{ Use: "install [sourceurl]", Short: `Install a konnector with the specified slug name from the given source URL.`, Long: ` Install a konnector with the specified slug name. You can also provide the version number to install a specific release if you use the registry:// scheme. Following formats are accepted: registry://// registry:/// registry:/// registry:// If you provide a channel and a version, the channel is ignored. Default channel is stable. Default version is latest. `, Example: ` $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable/1.0.1 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/1.2.0 $ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline `, RunE: func(cmd *cobra.Command, args []string) error { return installApp(cmd, args, consts.Konnectors) }, } var updateKonnectorCmd = &cobra.Command{ Use: "update [sourceurl]", Short: "Update the konnector with the specified slug name.", Aliases: []string{"upgrade"}, RunE: func(cmd *cobra.Command, args []string) error { return updateApp(cmd, args, consts.Konnectors) }, } var uninstallKonnectorCmd = &cobra.Command{ Use: "uninstall ", Short: "Uninstall the konnector with the specified slug name.", Aliases: []string{"rm"}, RunE: func(cmd *cobra.Command, args []string) error { return uninstallApp(cmd, args, consts.Konnectors) }, } var lsKonnectorsCmd = &cobra.Command{ Use: "ls", Short: "List the installed konnectors.", RunE: func(cmd *cobra.Command, args []string) error { return lsApps(cmd, args, consts.Konnectors) }, } var runKonnectorsCmd = &cobra.Command{ Use: "run ", Short: "Run a konnector.", Long: "Run a konnector named with specified slug using the specified options.", RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } if len(args) < 1 { return cmd.Usage() } slug := args[0] c := newClient(flagDomain, consts.Jobs+":POST:konnector:worker", consts.Triggers, consts.Files, consts.Accounts, ) ts, err := c.GetTriggers("konnector") if err != nil { return err } type localTrigger struct { id string accountID string } var triggers []*localTrigger for _, t := range ts { var msg struct { Slug string `json:"konnector"` Account string `json:"account"` } if err = json.Unmarshal(t.Attrs.Message, &msg); err != nil { return err } if msg.Slug == slug { triggers = append(triggers, &localTrigger{t.ID, msg.Account}) } } if len(triggers) == 0 { return fmt.Errorf("Could not find a konnector %q: "+ "it may be installed but it is not activated (no related trigger)", slug) } var trigger *localTrigger if len(triggers) > 1 || flagKonnectorAccountID != "" { if flagKonnectorAccountID == "" { return errors.New("Found multiple konnectors with different accounts: use the --account-id flag") } for _, t := range triggers { if t.accountID == flagKonnectorAccountID { trigger = t break } } if trigger == nil { return fmt.Errorf("Could not find konnector linked to account with id %q", flagKonnectorAccountID) } } else { trigger = triggers[0] } j, err := c.TriggerLaunch(trigger.id) if err != nil { return err } json, err := json.MarshalIndent(j, "", " ") if err != nil { return err } fmt.Println(string(json)) return nil }, } var listMaintenancesCmd = &cobra.Command{ Use: "ls-maintenances", Short: `List the konnectors in maintenance`, RunE: func(cmd *cobra.Command, args []string) (err error) { ac := newAdminClient() list, err := ac.ListMaintenances(flagKonnectorContext) if err != nil { return err } for _, item := range list { json, err := json.MarshalIndent(item, "", " ") if err != nil { return err } fmt.Println(string(json)) } return nil }, } var activateMaintenanceKonnectorsCmd = &cobra.Command{ Use: "maintenance [slug]", Short: `Activate the maintenance for the given konnector`, RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return cmd.Help() } messages := make(map[string]interface{}) for { locale := prompt("Locale (empty to abort):") if locale == "" { break } if len(locale) > 5 { fmt.Fprintf(os.Stdout, "Invalid locale name: %q\n", locale) continue } shortMessage := prompt("Short message:") longMessage := prompt("Long message:") messages[locale] = map[string]string{ "short_message": shortMessage, "long_message": longMessage, } } opts := map[string]interface{}{ "flag_short_maintenance": flagKonnectorsShortMaintenance, "flag_disallow_manual_exec": flagKonnectorsDisallowManualExec, "messages": messages, } ac := newAdminClient() return ac.ActivateMaintenance(args[0], opts) }, } var deactivateMaintenanceKonnectorsCmd = &cobra.Command{ Use: "deactivate-maintenance [slug]", Short: `Deactivate maintenance for the given konnector`, RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return cmd.Help() } ac := newAdminClient() return ac.DeactivateMaintenance(args[0]) }, } func installApp(cmd *cobra.Command, args []string, appType string) error { if len(args) < 1 { return cmd.Usage() } slug := args[0] source := "registry://" + slug + "/stable" if len(args) > 1 { source = args[1] } if flagAllDomains { return foreachDomains(func(in *client.Instance) error { c := newClient(in.Attrs.Domain, appType) _, err := c.InstallApp(&client.AppOptions{ AppType: appType, Slug: slug, SourceURL: source, Deactivated: flagAppsDeactivated, }) if err != nil { if err.Error() == app.ErrAlreadyExists.Error() { return nil } return err } fmt.Fprintf(os.Stdout, "Application installed successfully on %s\n", in.Attrs.Domain) return nil }) } if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } var overridenParameters *json.RawMessage if flagKonnectorsParameters != "" { tmp := json.RawMessage(flagKonnectorsParameters) overridenParameters = &tmp } c := newClient(flagDomain, appType) manifest, err := c.InstallApp(&client.AppOptions{ AppType: appType, Slug: slug, SourceURL: source, Deactivated: flagAppsDeactivated, OverridenParameters: overridenParameters, }) if err != nil { return err } fmt.Fprintf(os.Stdout, "%s (%s) has been installed on %s\n", slug, manifest.Attrs.Version, flagDomain) return nil } func updateApp(cmd *cobra.Command, args []string, appType string) error { if len(args) == 0 || len(args) > 2 { return cmd.Usage() } var src string if len(args) > 1 { src = args[1] } if flagAllDomains { return foreachDomains(func(in *client.Instance) error { c := newClient(in.Attrs.Domain, appType) _, err := c.UpdateApp(&client.AppOptions{ AppType: appType, Slug: args[0], SourceURL: src, }, flagSafeUpdate) if err != nil { if err.Error() == app.ErrNotFound.Error() { return nil } return err } fmt.Fprintf(os.Stdout, "Application updated successfully on %s\n", in.Attrs.Domain) return nil }) } if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } var overridenParameters *json.RawMessage if flagKonnectorsParameters != "" { tmp := json.RawMessage(flagKonnectorsParameters) overridenParameters = &tmp } c := newClient(flagDomain, appType) opts := &client.AppOptions{ AppType: appType, Slug: args[0], SourceURL: src, OverridenParameters: overridenParameters, } manifest, err := c.GetApp(opts) if err != nil { return err } newManifest, err := c.UpdateApp(opts, flagSafeUpdate) if err != nil { return err } msg := "%s is already up-to-date at %s\n" if app.IsMoreRecent(manifest.Attrs.Version, newManifest.Attrs.Version) { msg = "%s has been upgraded to %s\n" } else if app.IsMoreRecent(newManifest.Attrs.Version, manifest.Attrs.Version) { msg = "%s has been downgraded to %s\n" } fmt.Fprintf(os.Stdout, msg, args[0], newManifest.Attrs.Version) return nil } func uninstallApp(cmd *cobra.Command, args []string, appType string) error { if len(args) != 1 { return cmd.Usage() } if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } c := newClient(flagDomain, appType) manifest, err := c.UninstallApp(&client.AppOptions{ AppType: appType, Slug: args[0], }) if err != nil { return err } fmt.Fprintf(os.Stdout, "%s has been uninstalled\n", manifest.Attrs.Slug) return nil } func showApp(cmd *cobra.Command, args []string, appType string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } if len(args) < 1 { return cmd.Usage() } c := newClient(flagDomain, appType) manifest, err := c.GetApp(&client.AppOptions{ Slug: args[0], AppType: appType, }) if err != nil { return err } json, err := json.MarshalIndent(manifest.Attrs, "", " ") if err != nil { return err } fmt.Println(string(json)) return nil } func showWebAppTriggers(cmd *cobra.Command, args []string, appType string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } if len(args) < 1 { return cmd.Usage() } c := newClient(flagDomain, appType, consts.Triggers) manifest, err := c.GetApp(&client.AppOptions{ Slug: args[0], AppType: appType, }) if err != nil { return err } var triggerIDs []string if manifest.Attrs.Services == nil { fmt.Fprintf(os.Stdout, "No triggers\n") return nil } for _, service := range *manifest.Attrs.Services { if service.TriggerID == "" { continue } triggerIDs = append(triggerIDs, service.TriggerID) } var triggers []*client.Trigger var trigger *client.Trigger for _, triggerID := range triggerIDs { trigger, err = c.GetTrigger(triggerID) if err != nil { return err } triggers = append(triggers, trigger) } json, err := json.MarshalIndent(triggers, "", " ") if err != nil { return err } fmt.Println(string(json)) return nil } var listTriggerCmd = &cobra.Command{ Use: "ls", Short: `List triggers`, Example: "$ cozy-stack triggers ls --domain cozy.localhost:8080", RunE: func(cmd *cobra.Command, args []string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } c := newClient(flagDomain, consts.Triggers) list, err := c.ListTriggers() if err != nil { return err } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) for _, t := range list { fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n", t.ID, t.Attrs.WorkerType, t.Attrs.Type, t.Attrs.Arguments, t.Attrs.Debounce, ) } return w.Flush() }, } var launchTriggerCmd = &cobra.Command{ Use: "launch [triggerId]", Short: `Creates a job from a specific trigger`, Example: "$ cozy-stack triggers launch --domain cozy.localhost:8080 748f42b65aca8c99ec2492eb660d1891", RunE: launchTrigger, } func launchTrigger(cmd *cobra.Command, args []string) error { if len(args) < 1 { return cmd.Usage() } if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } // Creates client c := newClient(flagDomain, consts.Triggers) // Creates job j, err := c.TriggerLaunch(args[0]) if err != nil { return err } // Print JSON json, err := json.MarshalIndent(j, "", " ") if err != nil { return err } fmt.Println(string(json)) return nil } func lsApps(cmd *cobra.Command, args []string, appType string) error { if flagDomain == "" { errPrintfln("%s", errMissingDomain) return cmd.Usage() } c := newClient(flagDomain, appType) manifests, err := c.ListApps(appType) if err != nil { return err } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) for _, m := range manifests { fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", m.Attrs.Slug, m.Attrs.Source, m.Attrs.Version, m.Attrs.State, ) } return w.Flush() } func foreachDomains(predicate func(*client.Instance) error) error { ac := newAdminClient() list, err := ac.ListInstances() if err != nil { return nil } var hasErr bool for _, i := range list { if err = predicate(i); err != nil { errPrintfln("%s: %s", i.Attrs.Domain, err) hasErr = true } } if hasErr { return errors.New("At least one error occurred while executing this command") } return nil } func init() { webappsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") webappsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively") installWebappCmd.PersistentFlags().BoolVar(&flagAppsDeactivated, "ask-permissions", false, "specify that the application should not be activated after installation") updateWebappCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes") updateKonnectorCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes") runKonnectorsCmd.PersistentFlags().StringVar(&flagKonnectorAccountID, "account-id", "", "specify the account ID to use for running the konnector") listMaintenancesCmd.PersistentFlags().StringVar(&flagKonnectorContext, "context", "", "include konnectors in maintenance for apps registry of this context") activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsShortMaintenance, "short", false, "specify a short maintenance") activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsDisallowManualExec, "no-manual-exec", false, "specify a maintenance disallowing manual execution") triggersCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") triggersCmdGroup.AddCommand(launchTriggerCmd) triggersCmdGroup.AddCommand(listTriggerCmd) triggersCmdGroup.AddCommand(showWebappTriggersCmd) webappsCmdGroup.AddCommand(lsWebappsCmd) webappsCmdGroup.AddCommand(showWebappCmd) webappsCmdGroup.AddCommand(installWebappCmd) webappsCmdGroup.AddCommand(updateWebappCmd) webappsCmdGroup.AddCommand(uninstallWebappCmd) konnectorsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance") konnectorsCmdGroup.PersistentFlags().StringVar(&flagKonnectorsParameters, "parameters", "", "override the parameters of the installed konnector") konnectorsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively") konnectorsCmdGroup.AddCommand(lsKonnectorsCmd) konnectorsCmdGroup.AddCommand(showKonnectorCmd) konnectorsCmdGroup.AddCommand(installKonnectorCmd) konnectorsCmdGroup.AddCommand(updateKonnectorCmd) konnectorsCmdGroup.AddCommand(uninstallKonnectorCmd) konnectorsCmdGroup.AddCommand(runKonnectorsCmd) konnectorsCmdGroup.AddCommand(listMaintenancesCmd) konnectorsCmdGroup.AddCommand(activateMaintenanceKonnectorsCmd) konnectorsCmdGroup.AddCommand(deactivateMaintenanceKonnectorsCmd) RootCmd.AddCommand(triggersCmdGroup) RootCmd.AddCommand(webappsCmdGroup) RootCmd.AddCommand(konnectorsCmdGroup) }