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 }