github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/cmd/instances.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"reflect"
    15  	"strings"
    16  	"text/tabwriter"
    17  	"time"
    18  
    19  	"github.com/cozy/cozy-stack/client"
    20  	"github.com/cozy/cozy-stack/client/request"
    21  	build "github.com/cozy/cozy-stack/pkg/config"
    22  	"github.com/cozy/cozy-stack/pkg/consts"
    23  	"github.com/cozy/cozy-stack/pkg/couchdb"
    24  	humanize "github.com/dustin/go-humanize"
    25  	"github.com/spf13/cobra"
    26  )
    27  
    28  var flagDomainAliases []string
    29  var flagListFields []string
    30  var flagLocale string
    31  var flagTimezone string
    32  var flagEmail string
    33  var flagPublicName string
    34  var flagSettings string
    35  var flagDiskQuota string
    36  var flagApps []string
    37  var flagBlocked bool
    38  var flagBlockingReason string
    39  var flagDeleting bool
    40  var flagDev bool
    41  var flagTrace bool
    42  var flagPassphrase string
    43  var flagForce bool
    44  var flagJSON bool
    45  var flagSwiftLayout int
    46  var flagCouchCluster int
    47  var flagUUID string
    48  var flagOIDCID string
    49  var flagFranceConnectID string
    50  var flagMagicLink bool
    51  var flagTOSSigned string
    52  var flagTOS string
    53  var flagTOSLatest string
    54  var flagContextName string
    55  var flagSponsorships []string
    56  var flagOnboardingFinished bool
    57  var flagTTL time.Duration
    58  var flagExpire time.Duration
    59  var flagAllowLoginScope bool
    60  var flagAvailableFields bool
    61  var flagOnboardingSecret string
    62  var flagOnboardingApp string
    63  var flagOnboardingPermissions string
    64  var flagOnboardingState string
    65  var flagPath string
    66  
    67  // instanceCmdGroup represents the instances command
    68  var instanceCmdGroup = &cobra.Command{
    69  	Use:     "instances <command>",
    70  	Aliases: []string{"instance"},
    71  	Short:   "Manage instances of a stack",
    72  	Long: `
    73  cozy-stack instances allows to manage the instances of this stack
    74  
    75  An instance is a logical space owned by one user and identified by a domain.
    76  For example, bob.cozycloud.cc is the instance of Bob. A single cozy-stack
    77  process can manage several instances.
    78  
    79  Each instance has a separate space for storing files and a prefix used to
    80  create its CouchDB databases.
    81  `,
    82  	RunE: func(cmd *cobra.Command, args []string) error {
    83  		return cmd.Usage()
    84  	},
    85  }
    86  
    87  var showInstanceCmd = &cobra.Command{
    88  	Use:   "show <domain>",
    89  	Short: "Show the instance of the specified domain",
    90  	Long: `
    91  cozy-stack instances show allows to show the instance on the cozy for a
    92  given domain.
    93  `,
    94  	Example: "$ cozy-stack instances show cozy.localhost:8080",
    95  	RunE: func(cmd *cobra.Command, args []string) error {
    96  		if len(args) == 0 {
    97  			return cmd.Usage()
    98  		}
    99  		domain := args[0]
   100  		ac := newAdminClient()
   101  		in, err := ac.GetInstance(domain)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		json, err := json.MarshalIndent(in, "", "  ")
   106  		if err != nil {
   107  			return err
   108  		}
   109  		fmt.Println(string(json))
   110  		return nil
   111  	},
   112  }
   113  
   114  var showDBPrefixInstanceCmd = &cobra.Command{
   115  	Use:   "show-db-prefix <domain>",
   116  	Short: "Show the instance DB prefix of the specified domain",
   117  	Long: `
   118  cozy-stack instances show allows to show the instance prefix on the cozy for a
   119  given domain. The prefix is used for databases and VFS prefixing.
   120  
   121  It will also show the couch_cluster if it is not the default one.
   122  `,
   123  	Example: "$ cozy-stack instances show-db-prefix cozy.localhost:8080",
   124  	RunE: func(cmd *cobra.Command, args []string) error {
   125  		if len(args) == 0 {
   126  			return cmd.Usage()
   127  		}
   128  		domain := args[0]
   129  		ac := newAdminClient()
   130  		in, err := ac.GetInstance(domain)
   131  		if err != nil {
   132  			return err
   133  		}
   134  		if in.Attrs.Prefix != "" {
   135  			fmt.Println(in.Attrs.Prefix)
   136  		} else {
   137  			fmt.Println(couchdb.EscapeCouchdbName(in.Attrs.Domain))
   138  		}
   139  		if in.Attrs.CouchCluster != 0 {
   140  			fmt.Fprintf(os.Stdout, "couch_cluster: %d\n", in.Attrs.CouchCluster)
   141  		}
   142  		return nil
   143  	},
   144  }
   145  
   146  var addInstanceCmd = &cobra.Command{
   147  	Use:     "add <domain>",
   148  	Aliases: []string{"create"},
   149  	Short:   "Manage instances of a stack",
   150  	Long: `
   151  cozy-stack instances add allows to create an instance on the cozy for a
   152  given domain.
   153  
   154  If the COZY_DISABLE_INSTANCES_ADD_RM env variable is set, creating and
   155  destroying instances will be disabled and the content of this variable will
   156  be used as the error message.
   157  `,
   158  	Example: "$ cozy-stack instances add --passphrase cozy --apps drive,photos,settings,home,store cozy.localhost:8080",
   159  	RunE: func(cmd *cobra.Command, args []string) error {
   160  		if reason := os.Getenv("COZY_DISABLE_INSTANCES_ADD_RM"); reason != "" {
   161  			return fmt.Errorf("Sorry, instances add is disabled: %s", reason)
   162  		}
   163  		if len(args) == 0 {
   164  			return cmd.Usage()
   165  		}
   166  		if flagDev {
   167  			errPrintfln("The --dev flag has been deprecated")
   168  		}
   169  
   170  		var diskQuota int64
   171  		if flagDiskQuota != "" {
   172  			diskQuotaU, err := humanize.ParseBytes(flagDiskQuota)
   173  			if err != nil {
   174  				return err
   175  			}
   176  			diskQuota = int64(diskQuotaU)
   177  		}
   178  
   179  		domain := args[0]
   180  		fmt.Fprintf(os.Stdout, "Creating instance for domain \"%s\": please wait...\n", domain)
   181  		ac := newAdminClient()
   182  		in, err := ac.CreateInstance(&client.InstanceOptions{
   183  			Domain:          domain,
   184  			DomainAliases:   flagDomainAliases,
   185  			Locale:          flagLocale,
   186  			UUID:            flagUUID,
   187  			OIDCID:          flagOIDCID,
   188  			FranceConnectID: flagFranceConnectID,
   189  			TOSSigned:       flagTOSSigned,
   190  			Timezone:        flagTimezone,
   191  			ContextName:     flagContextName,
   192  			Sponsorships:    flagSponsorships,
   193  			Email:           flagEmail,
   194  			PublicName:      flagPublicName,
   195  			Settings:        flagSettings,
   196  			SwiftLayout:     flagSwiftLayout,
   197  			CouchCluster:    flagCouchCluster,
   198  			DiskQuota:       diskQuota,
   199  			Apps:            flagApps,
   200  			Passphrase:      flagPassphrase,
   201  			MagicLink:       &flagMagicLink,
   202  			Trace:           &flagTrace,
   203  		})
   204  		if err != nil {
   205  			errPrintfln(
   206  				"Failed to create instance for domain %s", domain)
   207  			return err
   208  		}
   209  
   210  		fmt.Fprintf(os.Stdout, "Instance created with success for domain %s\n", in.Attrs.Domain)
   211  		myProtocol := "https"
   212  		if build.IsDevRelease() {
   213  			myProtocol = "http"
   214  		}
   215  		if in.Attrs.RegisterToken != nil {
   216  			fmt.Fprintf(os.Stdout, "Registration token: \"%s\"\n", hex.EncodeToString(in.Attrs.RegisterToken))
   217  			fmt.Fprintf(os.Stdout, "Define your password by visiting %s://%s/?registerToken=%s\n", myProtocol, in.Attrs.Domain, hex.EncodeToString(in.Attrs.RegisterToken))
   218  		}
   219  		if len(flagApps) == 0 {
   220  			return nil
   221  		}
   222  		c, err := ac.NewInstanceClient(domain, consts.Apps)
   223  		if err != nil {
   224  			errPrintfln("Could not generate access to domain %s", domain)
   225  			errPrintfln("%s", err)
   226  			os.Exit(1)
   227  		}
   228  		apps, err := c.ListApps(consts.Apps)
   229  		if err == nil && len(flagApps) != len(apps) {
   230  			for _, slug := range flagApps {
   231  				found := false
   232  				for _, app := range apps {
   233  					if app.Attrs.Slug == slug {
   234  						found = true
   235  						break
   236  					}
   237  				}
   238  				if !found {
   239  					fmt.Fprintf(os.Stdout, "/!\\ Application %s has not been installed\n", slug)
   240  				}
   241  			}
   242  		}
   243  		return nil
   244  	},
   245  }
   246  
   247  var modifyInstanceCmd = &cobra.Command{
   248  	Use:   "modify <domain>",
   249  	Short: "Modify the instance properties",
   250  	Long: `
   251  cozy-stack instances modify allows to change the instance properties and
   252  settings for a specified domain.
   253  `,
   254  	RunE: func(cmd *cobra.Command, args []string) error {
   255  		if len(args) == 0 {
   256  			return cmd.Usage()
   257  		}
   258  
   259  		var diskQuota int64
   260  		if flagDiskQuota != "" {
   261  			diskQuotaU, err := humanize.ParseBytes(flagDiskQuota)
   262  			if err != nil {
   263  				return err
   264  			}
   265  			diskQuota = int64(diskQuotaU)
   266  		}
   267  
   268  		domain := args[0]
   269  		ac := newAdminClient()
   270  		opts := &client.InstanceOptions{
   271  			Domain:          domain,
   272  			DomainAliases:   flagDomainAliases,
   273  			Locale:          flagLocale,
   274  			UUID:            flagUUID,
   275  			OIDCID:          flagOIDCID,
   276  			FranceConnectID: flagFranceConnectID,
   277  			TOSSigned:       flagTOS,
   278  			TOSLatest:       flagTOSLatest,
   279  			Timezone:        flagTimezone,
   280  			ContextName:     flagContextName,
   281  			Sponsorships:    flagSponsorships,
   282  			Email:           flagEmail,
   283  			PublicName:      flagPublicName,
   284  			Settings:        flagSettings,
   285  			BlockingReason:  flagBlockingReason,
   286  			DiskQuota:       diskQuota,
   287  			MagicLink:       &flagMagicLink,
   288  		}
   289  		if flag := cmd.Flag("blocked"); flag.Changed {
   290  			opts.Blocked = &flagBlocked
   291  		}
   292  		if flag := cmd.Flag("deleting"); flag.Changed {
   293  			opts.Deleting = &flagDeleting
   294  		}
   295  		if flagOnboardingFinished {
   296  			opts.OnboardingFinished = &flagOnboardingFinished
   297  		}
   298  		in, err := ac.ModifyInstance(opts)
   299  		if err != nil {
   300  			errPrintfln(
   301  				"Failed to modify instance for domain %s", domain)
   302  			return err
   303  		}
   304  		json, err := json.MarshalIndent(in, "", "  ")
   305  		if err != nil {
   306  			return err
   307  		}
   308  		fmt.Println(string(json))
   309  		return nil
   310  	},
   311  }
   312  
   313  var updateInstancePassphraseCmd = &cobra.Command{
   314  	Use:     "set-passphrase <domain> <new-passphrase>",
   315  	Short:   "Change the passphrase of the instance",
   316  	Example: "$ cozy-stack instances set-passphrase cozy.localhost:8080 myN3wP4ssowrd!",
   317  	RunE: func(cmd *cobra.Command, args []string) error {
   318  		if len(args) != 2 {
   319  			return cmd.Usage()
   320  		}
   321  		domain := args[0]
   322  		c := newClient(domain, consts.Settings)
   323  		body := struct {
   324  			New   string `json:"new_passphrase"`
   325  			Force bool   `json:"force"`
   326  		}{
   327  			New:   args[1],
   328  			Force: true,
   329  		}
   330  
   331  		reqBody, err := json.Marshal(body)
   332  		if err != nil {
   333  			return err
   334  		}
   335  		res, err := c.Req(&request.Options{
   336  			Method: "PUT",
   337  			Path:   "/settings/passphrase",
   338  			Body:   bytes.NewReader(reqBody),
   339  			Headers: request.Headers{
   340  				"Content-Type": "application/json",
   341  			},
   342  		})
   343  		if err != nil {
   344  			return err
   345  		}
   346  
   347  		switch res.StatusCode {
   348  		case http.StatusNoContent:
   349  			fmt.Println("Passphrase has been changed for instance ", domain)
   350  		case http.StatusBadRequest:
   351  			return fmt.Errorf("Bad current passphrase for instance %s", domain)
   352  		case http.StatusInternalServerError:
   353  			return fmt.Errorf("%s", err)
   354  		}
   355  
   356  		return nil
   357  	},
   358  }
   359  
   360  var quotaInstanceCmd = &cobra.Command{
   361  	Use:   "set-disk-quota <domain> <disk-quota>",
   362  	Short: "Change the disk-quota of the instance",
   363  	Long: `
   364  cozy-stack instances set-disk-quota allows to change the disk-quota of the
   365  instance of the given domain. Set the quota to 0 to remove the quota.
   366  `,
   367  	Example: "$ cozy-stack instances set-disk-quota cozy.localhost:8080 3GB",
   368  	RunE: func(cmd *cobra.Command, args []string) error {
   369  		if len(args) != 2 {
   370  			return cmd.Usage()
   371  		}
   372  		parsed, err := humanize.ParseBytes(args[1])
   373  		if err != nil {
   374  			return fmt.Errorf("Could not parse disk-quota: %s", err)
   375  		}
   376  		diskQuota := int64(parsed)
   377  		if diskQuota == 0 {
   378  			diskQuota = -1
   379  		}
   380  		domain := args[0]
   381  		ac := newAdminClient()
   382  		_, err = ac.ModifyInstance(&client.InstanceOptions{
   383  			Domain:    domain,
   384  			DiskQuota: diskQuota,
   385  		})
   386  		return err
   387  	},
   388  }
   389  
   390  var debugInstanceCmd = &cobra.Command{
   391  	Use:   "debug <true/false>",
   392  	Short: "Activate or deactivate debugging of the instance",
   393  	Long: `
   394  cozy-stack instances debug allows to activate or deactivate the debugging of a
   395  specific domain.
   396  `,
   397  	Example: "$ cozy-stack instances debug --domain cozy.localhost:8080 true",
   398  	RunE: func(cmd *cobra.Command, args []string) error {
   399  		action := "enable"
   400  		domain := flagDomain
   401  		switch len(args) {
   402  		case 0:
   403  			action = "get"
   404  		case 1:
   405  			if strings.ToLower(args[0]) == "false" {
   406  				action = "disable"
   407  			}
   408  		case 2:
   409  			deprecatedDomainArg()
   410  			domain = args[0]
   411  			if strings.ToLower(args[1]) == "false" {
   412  				action = "disable"
   413  			}
   414  		default:
   415  			action = ""
   416  		}
   417  		if action == "" || domain == "" {
   418  			return cmd.Usage()
   419  		}
   420  
   421  		ac := newAdminClient()
   422  		var err error
   423  		var debug bool
   424  		switch action {
   425  		case "get":
   426  			debug, err = ac.GetDebug(domain)
   427  		case "enable":
   428  			err = ac.EnableDebug(domain, flagTTL)
   429  			debug = true
   430  		case "disable":
   431  			err = ac.DisableDebug(domain)
   432  			debug = false
   433  		}
   434  		if debug {
   435  			fmt.Fprintf(os.Stdout, "Debug is enabled on %s\n", domain)
   436  		} else {
   437  			fmt.Fprintf(os.Stdout, "Debug is disabled on %s\n", domain)
   438  		}
   439  		return err
   440  	},
   441  }
   442  
   443  var countInstanceCmd = &cobra.Command{
   444  	Use:     "count",
   445  	Short:   "Count the instances",
   446  	Example: "$ cozy-stack instances count",
   447  	RunE: func(cmd *cobra.Command, args []string) error {
   448  		ac := newAdminClient()
   449  		count, err := ac.CountInstances()
   450  		if err != nil {
   451  			return err
   452  		}
   453  		if count == 1 {
   454  			fmt.Fprintf(os.Stdout, "%d instance\n", count)
   455  		} else {
   456  			fmt.Fprintf(os.Stdout, "%d instances\n", count)
   457  		}
   458  		return nil
   459  	},
   460  }
   461  
   462  var lsInstanceCmd = &cobra.Command{
   463  	Use:     "ls",
   464  	Aliases: []string{"list"},
   465  	Short:   "List instances",
   466  	Long: `
   467  cozy-stack instances ls allows to list all the instances that can be served
   468  by this server.
   469  `,
   470  	Example: "$ cozy-stack instances ls",
   471  	RunE: func(cmd *cobra.Command, args []string) error {
   472  		if flagAvailableFields {
   473  			instance := &client.Instance{}
   474  			val := reflect.ValueOf(instance.Attrs)
   475  			t := val.Type()
   476  			for i := 0; i < t.NumField(); i++ {
   477  				param := t.Field(i).Tag.Get("json")
   478  				param = strings.TrimSuffix(param, ",omitempty")
   479  				param = strings.TrimSuffix(param, ",string")
   480  				fmt.Println(param)
   481  			}
   482  			fmt.Println("db_prefix")
   483  			return nil
   484  		}
   485  		ac := newAdminClient()
   486  		list, err := ac.ListInstances()
   487  		if err != nil {
   488  			return err
   489  		}
   490  		if flagJSON {
   491  			if len(flagListFields) > 0 {
   492  				for _, inst := range list {
   493  					var values map[string]interface{}
   494  					values, err = extractFields(inst.Attrs, flagListFields)
   495  					if err != nil {
   496  						return err
   497  					}
   498  
   499  					// Insert the db_prefix value if needed
   500  					for _, v := range flagListFields {
   501  						if v == "db_prefix" {
   502  							values["db_prefix"] = couchdb.EscapeCouchdbName(inst.DBPrefix())
   503  						}
   504  					}
   505  
   506  					m := make(map[string]interface{}, len(flagListFields))
   507  					for _, fieldName := range flagListFields {
   508  						if v, ok := values[fieldName]; ok {
   509  							m[fieldName] = v
   510  						} else {
   511  							m[fieldName] = nil
   512  						}
   513  					}
   514  
   515  					if err = json.NewEncoder(os.Stdout).Encode(m); err != nil {
   516  						return err
   517  					}
   518  				}
   519  			} else {
   520  				for _, inst := range list {
   521  					if err = json.NewEncoder(os.Stdout).Encode(inst.Attrs); err != nil {
   522  						return err
   523  					}
   524  				}
   525  			}
   526  		} else {
   527  			w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   528  			if len(flagListFields) > 0 {
   529  				format := strings.Repeat("%v\t", len(flagListFields))
   530  				format = format[:len(format)-1] + "\n"
   531  				for _, inst := range list {
   532  					var values map[string]interface{}
   533  					var instancesLines []interface{}
   534  
   535  					values, err = extractFields(inst.Attrs, flagListFields)
   536  					if err != nil {
   537  						return err
   538  					}
   539  
   540  					// Insert the db_prefix value if needed
   541  					for _, v := range flagListFields {
   542  						if v == "db_prefix" {
   543  							values["db_prefix"] = couchdb.EscapeCouchdbName(inst.DBPrefix())
   544  						}
   545  					}
   546  					// We append to a list to print in the same order as
   547  					// requested
   548  					for _, fieldName := range flagListFields {
   549  						instancesLines = append(instancesLines, values[fieldName])
   550  					}
   551  
   552  					fmt.Fprintf(w, format, instancesLines...)
   553  				}
   554  			} else {
   555  				for _, i := range list {
   556  					prefix := i.Attrs.Prefix
   557  					DBPrefix := prefix
   558  					if prefix == "" {
   559  						DBPrefix = couchdb.EscapeCouchdbName(i.Attrs.Domain)
   560  					}
   561  					fmt.Fprintf(w, "%s\t%s\t%s\t%s\tv%d\t%s\t%s\n",
   562  						i.Attrs.Domain,
   563  						i.Attrs.Locale,
   564  						formatSize(i.Attrs.BytesDiskQuota),
   565  						formatOnboarded(i),
   566  						i.Attrs.IndexViewsVersion,
   567  						prefix,
   568  						DBPrefix,
   569  					)
   570  				}
   571  			}
   572  			w.Flush()
   573  		}
   574  		return nil
   575  	},
   576  }
   577  
   578  func extractFields(data interface{}, fieldsNames []string) (values map[string]interface{}, err error) {
   579  	var m map[string]interface{}
   580  	var b []byte
   581  	b, err = json.Marshal(data)
   582  	if err != nil {
   583  		return
   584  	}
   585  	if err = json.Unmarshal(b, &m); err != nil {
   586  		return
   587  	}
   588  	values = make(map[string]interface{}, len(fieldsNames))
   589  	for _, fieldName := range fieldsNames {
   590  		if v, ok := m[fieldName]; ok {
   591  			values[fieldName] = v
   592  		}
   593  	}
   594  	return
   595  }
   596  
   597  func formatSize(size int64) string {
   598  	if size == 0 {
   599  		return "unlimited"
   600  	}
   601  	return humanize.Bytes(uint64(size))
   602  }
   603  
   604  func formatOnboarded(i *client.Instance) string {
   605  	if i.Attrs.OnboardingFinished {
   606  		return "onboarded"
   607  	}
   608  	if len(i.Attrs.RegisterToken) > 0 {
   609  		return "onboarding"
   610  	}
   611  	return "pending"
   612  }
   613  
   614  var destroyInstanceCmd = &cobra.Command{
   615  	Use:   "destroy <domain>",
   616  	Short: "Remove instance",
   617  	Long: `
   618  cozy-stack instances destroy allows to remove an instance
   619  and all its data.
   620  `,
   621  	Aliases: []string{"rm", "delete", "remove"},
   622  	RunE: func(cmd *cobra.Command, args []string) error {
   623  		if reason := os.Getenv("COZY_DISABLE_INSTANCES_ADD_RM"); reason != "" {
   624  			return fmt.Errorf("Sorry, instances add is disabled: %s", reason)
   625  		}
   626  		if len(args) == 0 {
   627  			return cmd.Usage()
   628  		}
   629  
   630  		domain := args[0]
   631  
   632  		if !flagForce {
   633  			if err := confirmDomain("remove", domain); err != nil {
   634  				return err
   635  			}
   636  		}
   637  
   638  		ac := newAdminClient()
   639  		err := ac.DestroyInstance(domain)
   640  		if err != nil {
   641  			errPrintfln(
   642  				"An error occurred while destroying instance for domain %s", domain)
   643  			return err
   644  		}
   645  
   646  		fmt.Fprintf(os.Stdout, "Instance for domain %s has been destroyed with success\n", domain)
   647  		return nil
   648  	},
   649  }
   650  
   651  func confirmDomain(action, domain string) error {
   652  	reader := bufio.NewReader(os.Stdin)
   653  	fmt.Fprintf(os.Stdout, `Are you sure you want to %s instance for domain %s?
   654  All data associated with this domain will be permanently lost.
   655  Type again the domain to confirm: `, action, domain)
   656  
   657  	str, err := reader.ReadString('\n')
   658  	if err != nil {
   659  		return err
   660  	}
   661  
   662  	str = strings.ToLower(strings.TrimSpace(str))
   663  	if str != domain {
   664  		return errors.New("Aborted")
   665  	}
   666  
   667  	fmt.Println()
   668  	return nil
   669  }
   670  
   671  var fsckInstanceCmd = &cobra.Command{
   672  	Use:   "fsck <domain>",
   673  	Short: "Check a vfs",
   674  	Long: `
   675  The cozy-stack fsck command checks that the files in the VFS are not
   676  desynchronized, ie a file present in CouchDB but not swift/localfs, or present
   677  in swift/localfs but not couchdb.
   678  
   679  There are 2 steps:
   680  
   681  - index integrity checks that there are nothing wrong in the index (CouchDB),
   682    like a file present in a directory that has been deleted
   683  - files consistency checks that the files are the same in the index (CouchDB)
   684    and the storage (Swift or localfs).
   685  
   686  By default, both operations are done, but you can choose one or the other via
   687  the flags.
   688  `,
   689  	RunE: func(cmd *cobra.Command, args []string) error {
   690  		errPrintfln("Please use cozy-stack check fs, this command has been deprecated")
   691  		if len(args) == 0 {
   692  			return cmd.Usage()
   693  		}
   694  		return fsck(args[0])
   695  	},
   696  }
   697  
   698  func appOrKonnectorTokenInstance(cmd *cobra.Command, args []string, appType string) error {
   699  	if len(args) < 2 {
   700  		return cmd.Usage()
   701  	}
   702  	ac := newAdminClient()
   703  	token, err := ac.GetToken(&client.TokenOptions{
   704  		Domain:   args[0],
   705  		Subject:  args[1],
   706  		Audience: appType,
   707  		Expire:   &flagExpire,
   708  	})
   709  	if err != nil {
   710  		return err
   711  	}
   712  	_, err = fmt.Println(token)
   713  	return err
   714  }
   715  
   716  var appTokenInstanceCmd = &cobra.Command{
   717  	Use:   "token-app <domain> <slug>",
   718  	Short: "Generate a new application token",
   719  	RunE: func(cmd *cobra.Command, args []string) error {
   720  		return appOrKonnectorTokenInstance(cmd, args, "app")
   721  	},
   722  }
   723  
   724  var konnectorTokenInstanceCmd = &cobra.Command{
   725  	Use:   "token-konnector <domain> <slug>",
   726  	Short: "Generate a new konnector token",
   727  	RunE: func(cmd *cobra.Command, args []string) error {
   728  		return appOrKonnectorTokenInstance(cmd, args, "konn")
   729  	},
   730  }
   731  
   732  var cliTokenInstanceCmd = &cobra.Command{
   733  	Use:   "token-cli <domain> <scopes>",
   734  	Short: "Generate a new CLI access token (global access)",
   735  	RunE: func(cmd *cobra.Command, args []string) error {
   736  		if len(args) < 2 {
   737  			return cmd.Usage()
   738  		}
   739  		ac := newAdminClient()
   740  		token, err := ac.GetToken(&client.TokenOptions{
   741  			Domain:   args[0],
   742  			Scope:    args[1:],
   743  			Audience: consts.CLIAudience,
   744  		})
   745  		if err != nil {
   746  			return err
   747  		}
   748  		_, err = fmt.Println(token)
   749  		return err
   750  	},
   751  }
   752  
   753  var oauthTokenInstanceCmd = &cobra.Command{
   754  	Use:     "token-oauth <domain> <clientid> <scopes>",
   755  	Short:   "Generate a new OAuth access token",
   756  	Example: "$ cozy-stack instances token-oauth cozy.localhost:8080 727e677187a51d14ccd59cc0bd000a1d io.cozy.files io.cozy.jobs:POST:sendmail:worker",
   757  	RunE: func(cmd *cobra.Command, args []string) error {
   758  		if len(args) < 3 {
   759  			return cmd.Usage()
   760  		}
   761  		if args[1] == "" {
   762  			return errors.New("Missing clientID")
   763  		}
   764  		if strings.Contains(args[2], ",") {
   765  			fmt.Fprintf(os.Stderr, "Warning: the delimiter for the scopes is a space!\n")
   766  		}
   767  		ac := newAdminClient()
   768  		token, err := ac.GetToken(&client.TokenOptions{
   769  			Domain:   args[0],
   770  			Subject:  args[1],
   771  			Audience: consts.AccessTokenAudience,
   772  			Scope:    args[2:],
   773  			Expire:   &flagExpire,
   774  		})
   775  		if err != nil {
   776  			return err
   777  		}
   778  		_, err = fmt.Println(token)
   779  		return err
   780  	},
   781  }
   782  
   783  var oauthRefreshTokenInstanceCmd = &cobra.Command{
   784  	Use:     "refresh-token-oauth <domain> <clientid> <scopes>",
   785  	Short:   "Generate a new OAuth refresh token",
   786  	Example: "$ cozy-stack instances refresh-token-oauth cozy.localhost:8080 727e677187a51d14ccd59cc0bd000a1d io.cozy.files io.cozy.jobs:POST:sendmail:worker",
   787  	RunE: func(cmd *cobra.Command, args []string) error {
   788  		if len(args) < 3 {
   789  			return cmd.Usage()
   790  		}
   791  		if strings.Contains(args[2], ",") {
   792  			fmt.Fprintf(os.Stderr, "Warning: the delimiter for the scopes is a space!\n")
   793  		}
   794  		ac := newAdminClient()
   795  		token, err := ac.GetToken(&client.TokenOptions{
   796  			Domain:   args[0],
   797  			Subject:  args[1],
   798  			Audience: consts.RefreshTokenAudience,
   799  			Scope:    args[2:],
   800  		})
   801  		if err != nil {
   802  			return err
   803  		}
   804  		_, err = fmt.Println(token)
   805  		return err
   806  	},
   807  }
   808  
   809  var oauthClientInstanceCmd = &cobra.Command{
   810  	Use:   "client-oauth <domain> <redirect_uri> <client_name> <software_id>",
   811  	Short: "Register a new OAuth client",
   812  	Long:  `It registers a new OAuth client and returns its client_id`,
   813  	RunE: func(cmd *cobra.Command, args []string) error {
   814  		if len(args) < 4 {
   815  			return cmd.Usage()
   816  		}
   817  		ac := newAdminClient()
   818  		oauthClient, err := ac.RegisterOAuthClient(&client.OAuthClientOptions{
   819  			Domain:                args[0],
   820  			RedirectURI:           args[1],
   821  			ClientName:            args[2],
   822  			SoftwareID:            args[3],
   823  			AllowLoginScope:       flagAllowLoginScope,
   824  			OnboardingSecret:      flagOnboardingSecret,
   825  			OnboardingApp:         flagOnboardingApp,
   826  			OnboardingPermissions: flagOnboardingPermissions,
   827  			OnboardingState:       flagOnboardingState,
   828  		})
   829  		if err != nil {
   830  			return err
   831  		}
   832  		if flagJSON {
   833  			encoder := json.NewEncoder(os.Stdout)
   834  			encoder.SetIndent("", "\t")
   835  			err = encoder.Encode(oauthClient)
   836  		} else {
   837  			_, err = fmt.Println(oauthClient["client_id"])
   838  		}
   839  		return err
   840  	},
   841  }
   842  
   843  var findOauthClientCmd = &cobra.Command{
   844  	Use:   "find-oauth-client <domain> <software_id>",
   845  	Short: "Find an OAuth client",
   846  	Long:  `Search an OAuth client from its SoftwareID`,
   847  	RunE: func(cmd *cobra.Command, args []string) error {
   848  		if len(args) < 2 {
   849  			return cmd.Usage()
   850  		}
   851  		var v interface{}
   852  		ac := newAdminClient()
   853  
   854  		q := url.Values{
   855  			"domain":      {args[0]},
   856  			"software_id": {args[1]},
   857  		}
   858  
   859  		req := &request.Options{
   860  			Method:  "GET",
   861  			Path:    "instances/oauth_client",
   862  			Queries: q,
   863  		}
   864  		res, err := ac.Req(req)
   865  		if err != nil {
   866  			return err
   867  		}
   868  		errd := json.NewDecoder(res.Body).Decode(&v)
   869  		if err != nil {
   870  			return errd
   871  		}
   872  		json, err := json.MarshalIndent(v, "", "  ")
   873  		if err != nil {
   874  			return err
   875  		}
   876  		fmt.Println(string(json))
   877  
   878  		return err
   879  	},
   880  }
   881  
   882  var exportCmd = &cobra.Command{
   883  	Use:   "export",
   884  	Short: "Export an instance",
   885  	Long:  `Export the files, documents, and settings`,
   886  	RunE: func(cmd *cobra.Command, args []string) error {
   887  		ac := newAdminClient()
   888  		return ac.Export(&client.ExportOptions{
   889  			Domain:    flagDomain,
   890  			LocalPath: flagPath,
   891  		})
   892  	},
   893  }
   894  
   895  var importCmd = &cobra.Command{
   896  	Use:   "import <URL>",
   897  	Short: "Import data from an export link",
   898  	Long:  "This command will reset the Cozy instance and import data from an export link",
   899  	RunE: func(cmd *cobra.Command, args []string) error {
   900  		ac := newAdminClient()
   901  		if len(args) < 1 {
   902  			return errors.New("The URL to the exported data is missing")
   903  		}
   904  
   905  		if !flagForce {
   906  			if err := confirmDomain("reset", flagDomain); err != nil {
   907  				return err
   908  			}
   909  		}
   910  
   911  		return ac.Import(flagDomain, &client.ImportOptions{
   912  			ManifestURL: args[0],
   913  		})
   914  	},
   915  }
   916  
   917  var showSwiftPrefixInstanceCmd = &cobra.Command{
   918  	Use:     "show-swift-prefix <domain>",
   919  	Short:   "Show the instance swift prefix of the specified domain",
   920  	Example: "$ cozy-stack instances show-swift-prefix cozy.localhost:8080",
   921  	RunE: func(cmd *cobra.Command, args []string) error {
   922  		var v map[string]string
   923  
   924  		ac := newAdminClient()
   925  		if len(args) < 1 {
   926  			return errors.New("The domain is missing")
   927  		}
   928  
   929  		req := &request.Options{
   930  			Method: "GET",
   931  			Path:   "instances/" + args[0] + "/swift-prefix",
   932  		}
   933  		res, err := ac.Req(req)
   934  		if err != nil {
   935  			return err
   936  		}
   937  		errd := json.NewDecoder(res.Body).Decode(&v)
   938  		if errd != nil {
   939  			return errd
   940  		}
   941  		json, errj := json.MarshalIndent(v, "", "  ")
   942  		if errj != nil {
   943  			return errj
   944  		}
   945  		fmt.Println(string(json))
   946  
   947  		return nil
   948  	},
   949  }
   950  
   951  var instanceAppVersionCmd = &cobra.Command{
   952  	Use:     "show-app-version [app-slug] [version]",
   953  	Short:   `Show instances that have a particular app version`,
   954  	Example: "$ cozy-stack instances show-app-version drive 1.0.1",
   955  	RunE: func(cmd *cobra.Command, args []string) error {
   956  		if len(args) != 2 {
   957  			return cmd.Usage()
   958  		}
   959  
   960  		ac := newAdminClient()
   961  		path := fmt.Sprintf("/instances/with-app-version/%s/%s", args[0], args[1])
   962  		res, err := ac.Req(&request.Options{
   963  			Method: "GET",
   964  			Path:   path,
   965  		})
   966  
   967  		if err != nil {
   968  			return err
   969  		}
   970  
   971  		out := struct {
   972  			Instances []string `json:"instances"`
   973  		}{}
   974  
   975  		err = json.NewDecoder(res.Body).Decode(&out)
   976  		if err != nil {
   977  			return err
   978  		}
   979  		if len(out.Instances) == 0 {
   980  			return fmt.Errorf("No instances have application \"%s\" in version \"%s\"", args[0], args[1])
   981  		}
   982  
   983  		json, err := json.MarshalIndent(out.Instances, "", "  ")
   984  		if err != nil {
   985  			return err
   986  		}
   987  		fmt.Println(string(json))
   988  		return nil
   989  	},
   990  }
   991  
   992  var setAuthModeCmd = &cobra.Command{
   993  	Use:     "auth-mode [domain] [auth-mode]",
   994  	Short:   `Set instance auth-mode`,
   995  	Example: "$ cozy-stack instances auth-mode cozy.localhost:8080 two_factor_mail",
   996  	Long: `Change the authentication mode for an instance. Two options are allowed:
   997  - two_factor_mail
   998  - basic
   999  `,
  1000  	RunE: func(cmd *cobra.Command, args []string) error {
  1001  		if len(args) != 2 {
  1002  			return cmd.Usage()
  1003  		}
  1004  
  1005  		domain := args[0]
  1006  		ac := newAdminClient()
  1007  
  1008  		body := struct {
  1009  			AuthMode string `json:"auth_mode"`
  1010  		}{
  1011  			AuthMode: args[1],
  1012  		}
  1013  
  1014  		reqBody, err := json.Marshal(body)
  1015  		if err != nil {
  1016  			return err
  1017  		}
  1018  
  1019  		res, err := ac.Req(&request.Options{
  1020  			Method: "POST",
  1021  			Path:   "/instances/" + url.PathEscape(domain) + "/auth-mode",
  1022  			Body:   bytes.NewReader(reqBody),
  1023  			Headers: request.Headers{
  1024  				"Content-Type": "application/json",
  1025  			},
  1026  		})
  1027  		if err != nil {
  1028  			return err
  1029  		}
  1030  		if res.StatusCode == http.StatusNoContent {
  1031  			fmt.Fprintf(os.Stdout, "Auth mode has been changed for %s\n", domain)
  1032  		} else {
  1033  			resBody, err := io.ReadAll(res.Body)
  1034  			if err != nil {
  1035  				return err
  1036  			}
  1037  			fmt.Println(string(resBody))
  1038  		}
  1039  		return nil
  1040  	},
  1041  }
  1042  
  1043  var cleanSessionsCmd = &cobra.Command{
  1044  	Use:     "clean-sessions <domain>",
  1045  	Short:   "Remove the io.cozy.sessions and io.cozy.sessions.logins bases",
  1046  	Example: "$ cozy-stack instance clean-sessions cozy.localhost:8080",
  1047  	RunE: func(cmd *cobra.Command, args []string) error {
  1048  		if len(args) == 0 {
  1049  			return cmd.Usage()
  1050  		}
  1051  		domain := args[0]
  1052  		ac := newAdminClient()
  1053  		return ac.CleanSessions(domain)
  1054  	},
  1055  }
  1056  
  1057  func init() {
  1058  	instanceCmdGroup.AddCommand(showInstanceCmd)
  1059  	instanceCmdGroup.AddCommand(showDBPrefixInstanceCmd)
  1060  	instanceCmdGroup.AddCommand(addInstanceCmd)
  1061  	instanceCmdGroup.AddCommand(modifyInstanceCmd)
  1062  	instanceCmdGroup.AddCommand(countInstanceCmd)
  1063  	instanceCmdGroup.AddCommand(lsInstanceCmd)
  1064  	instanceCmdGroup.AddCommand(quotaInstanceCmd)
  1065  	instanceCmdGroup.AddCommand(debugInstanceCmd)
  1066  	instanceCmdGroup.AddCommand(destroyInstanceCmd)
  1067  	instanceCmdGroup.AddCommand(fsckInstanceCmd)
  1068  	instanceCmdGroup.AddCommand(appTokenInstanceCmd)
  1069  	instanceCmdGroup.AddCommand(konnectorTokenInstanceCmd)
  1070  	instanceCmdGroup.AddCommand(cliTokenInstanceCmd)
  1071  	instanceCmdGroup.AddCommand(oauthTokenInstanceCmd)
  1072  	instanceCmdGroup.AddCommand(oauthRefreshTokenInstanceCmd)
  1073  	instanceCmdGroup.AddCommand(oauthClientInstanceCmd)
  1074  	instanceCmdGroup.AddCommand(findOauthClientCmd)
  1075  	instanceCmdGroup.AddCommand(exportCmd)
  1076  	instanceCmdGroup.AddCommand(importCmd)
  1077  	instanceCmdGroup.AddCommand(showSwiftPrefixInstanceCmd)
  1078  	instanceCmdGroup.AddCommand(instanceAppVersionCmd)
  1079  	instanceCmdGroup.AddCommand(updateInstancePassphraseCmd)
  1080  	instanceCmdGroup.AddCommand(setAuthModeCmd)
  1081  	instanceCmdGroup.AddCommand(cleanSessionsCmd)
  1082  	addInstanceCmd.Flags().StringSliceVar(&flagDomainAliases, "domain-aliases", nil, "Specify one or more aliases domain for the instance (separated by ',')")
  1083  	addInstanceCmd.Flags().StringVar(&flagLocale, "locale", consts.DefaultLocale, "Locale of the new cozy instance")
  1084  	addInstanceCmd.Flags().StringVar(&flagUUID, "uuid", "", "The UUID of the instance")
  1085  	addInstanceCmd.Flags().StringVar(&flagOIDCID, "oidc_id", "", "The identifier for checking authentication from OIDC")
  1086  	addInstanceCmd.Flags().StringVar(&flagFranceConnectID, "franceconnect_id", "", "The identifier for checking authentication with FranceConnect")
  1087  	addInstanceCmd.Flags().BoolVar(&flagMagicLink, "magic_link", false, "Enable authentication with magic links sent by email")
  1088  	addInstanceCmd.Flags().StringVar(&flagTOS, "tos", "", "The TOS version signed")
  1089  	addInstanceCmd.Flags().StringVar(&flagTimezone, "tz", "", "The timezone for the user")
  1090  	addInstanceCmd.Flags().StringVar(&flagContextName, "context-name", "", "Context name of the instance")
  1091  	addInstanceCmd.Flags().StringSliceVar(&flagSponsorships, "sponsorships", nil, "Sponsorships of the instance (comma separated list)")
  1092  	addInstanceCmd.Flags().StringVar(&flagEmail, "email", "", "The email of the owner")
  1093  	addInstanceCmd.Flags().StringVar(&flagPublicName, "public-name", "", "The public name of the owner")
  1094  	addInstanceCmd.Flags().StringVar(&flagSettings, "settings", "", "A list of settings (eg context:foo,offer:premium)")
  1095  	addInstanceCmd.Flags().IntVar(&flagSwiftLayout, "swift-layout", -1, "Specify the layout to use for Swift (from 0 for layout V1 to 2 for layout V3, -1 means the default)")
  1096  	addInstanceCmd.Flags().IntVar(&flagCouchCluster, "couch-cluster", -1, "Specify the CouchDB cluster where the instance will be created (-1 means the default)")
  1097  	addInstanceCmd.Flags().StringVar(&flagDiskQuota, "disk-quota", "", "The quota allowed to the instance's VFS")
  1098  	addInstanceCmd.Flags().StringSliceVar(&flagApps, "apps", nil, "Apps to be preinstalled")
  1099  	addInstanceCmd.Flags().BoolVar(&flagDev, "dev", false, "To create a development instance (deprecated)")
  1100  	addInstanceCmd.Flags().BoolVar(&flagTrace, "trace", false, "Show where time is spent")
  1101  	addInstanceCmd.Flags().StringVar(&flagPassphrase, "passphrase", "", "Register the instance with this passphrase (useful for tests)")
  1102  	modifyInstanceCmd.Flags().StringSliceVar(&flagDomainAliases, "domain-aliases", nil, "Specify one or more aliases domain for the instance (separated by ',')")
  1103  	modifyInstanceCmd.Flags().StringVar(&flagLocale, "locale", "", "New locale")
  1104  	modifyInstanceCmd.Flags().StringVar(&flagUUID, "uuid", "", "New UUID")
  1105  	modifyInstanceCmd.Flags().StringVar(&flagOIDCID, "oidc_id", "", "New identifier for checking authentication from OIDC")
  1106  	modifyInstanceCmd.Flags().StringVar(&flagFranceConnectID, "franceconnect_id", "", "The identifier for checking authentication with FranceConnect")
  1107  	modifyInstanceCmd.Flags().BoolVar(&flagMagicLink, "magic_link", false, "Enable authentication with magic links sent by email")
  1108  	modifyInstanceCmd.Flags().StringVar(&flagTOS, "tos", "", "Update the TOS version signed")
  1109  	modifyInstanceCmd.Flags().StringVar(&flagTOSLatest, "tos-latest", "", "Update the latest TOS version")
  1110  	modifyInstanceCmd.Flags().StringVar(&flagTimezone, "tz", "", "New timezone")
  1111  	modifyInstanceCmd.Flags().StringVar(&flagContextName, "context-name", "", "New context name")
  1112  	modifyInstanceCmd.Flags().StringSliceVar(&flagSponsorships, "sponsorships", nil, "Sponsorships of the instance (comma separated list)")
  1113  	modifyInstanceCmd.Flags().StringVar(&flagEmail, "email", "", "New email")
  1114  	modifyInstanceCmd.Flags().StringVar(&flagPublicName, "public-name", "", "New public name")
  1115  	modifyInstanceCmd.Flags().StringVar(&flagSettings, "settings", "", "New list of settings (eg offer:premium)")
  1116  	modifyInstanceCmd.Flags().StringVar(&flagDiskQuota, "disk-quota", "", "Specify a new disk quota")
  1117  	modifyInstanceCmd.Flags().StringVar(&flagBlockingReason, "blocking-reason", "", "Code that explains why the instance is blocked (PAYMENT_FAILED, LOGIN_FAILED, etc.)")
  1118  	modifyInstanceCmd.Flags().BoolVar(&flagBlocked, "blocked", false, "Block the instance")
  1119  	modifyInstanceCmd.Flags().BoolVar(&flagDeleting, "deleting", false, "Set (or remove) the deleting flag (ex: `--deleting=false`)")
  1120  	modifyInstanceCmd.Flags().BoolVar(&flagOnboardingFinished, "onboarding-finished", false, "Force the finishing of the onboarding")
  1121  	destroyInstanceCmd.Flags().BoolVar(&flagForce, "force", false, "Force the deletion without asking for confirmation")
  1122  	debugInstanceCmd.Flags().StringVar(&flagDomain, "domain", cozyDomain(), "Specify the domain name of the instance")
  1123  	debugInstanceCmd.Flags().DurationVar(&flagTTL, "ttl", 24*time.Hour, "Specify how long the debug mode will last")
  1124  	fsckInstanceCmd.Flags().BoolVar(&flagCheckFSIndexIntegrity, "index-integrity", false, "Check the index integrity only")
  1125  	fsckInstanceCmd.Flags().BoolVar(&flagCheckFSFilesConsistensy, "files-consistency", false, "Check the files consistency only (between CouchDB and Swift)")
  1126  	fsckInstanceCmd.Flags().BoolVar(&flagCheckFSFailFast, "fail-fast", false, "Stop the FSCK on the first error")
  1127  	fsckInstanceCmd.Flags().BoolVar(&flagJSON, "json", false, "Output more informations in JSON format")
  1128  	oauthClientInstanceCmd.Flags().BoolVar(&flagJSON, "json", false, "Output more informations in JSON format")
  1129  	oauthClientInstanceCmd.Flags().BoolVar(&flagAllowLoginScope, "allow-login-scope", false, "Allow login scope")
  1130  	oauthClientInstanceCmd.Flags().StringVar(&flagOnboardingSecret, "onboarding-secret", "", "Specify an OnboardingSecret")
  1131  	oauthClientInstanceCmd.Flags().StringVar(&flagOnboardingApp, "onboarding-app", "", "Specify an OnboardingApp")
  1132  	oauthClientInstanceCmd.Flags().StringVar(&flagOnboardingPermissions, "onboarding-permissions", "", "Specify an OnboardingPermissions")
  1133  	oauthClientInstanceCmd.Flags().StringVar(&flagOnboardingState, "onboarding-state", "", "Specify an OnboardingState")
  1134  	oauthTokenInstanceCmd.Flags().DurationVar(&flagExpire, "expire", 0, "Make the token expires in this amount of time, as a duration string, e.g. \"1h\"")
  1135  	appTokenInstanceCmd.Flags().DurationVar(&flagExpire, "expire", 0, "Make the token expires in this amount of time")
  1136  	lsInstanceCmd.Flags().BoolVar(&flagJSON, "json", false, "Show each line as a json representation of the instance")
  1137  	lsInstanceCmd.Flags().StringSliceVar(&flagListFields, "fields", nil, "Arguments shown for each line in the list")
  1138  	lsInstanceCmd.Flags().BoolVar(&flagAvailableFields, "available-fields", false, "List available fields for --fields option")
  1139  	exportCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
  1140  	exportCmd.Flags().StringVar(&flagPath, "path", "", "Specify the local path where to store the export archive")
  1141  	importCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
  1142  	importCmd.Flags().BoolVar(&flagForce, "force", false, "Force the import without asking for confirmation")
  1143  	_ = exportCmd.MarkFlagRequired("domain")
  1144  	_ = importCmd.MarkFlagRequired("domain")
  1145  	RootCmd.AddCommand(instanceCmdGroup)
  1146  }