github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/cli/credentials/init.go (about) 1 // This file is part of arduino-cloud-cli. 2 // 3 // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 package credentials 19 20 import ( 21 "errors" 22 "fmt" 23 "os" 24 "strings" 25 26 "github.com/arduino/arduino-cli/cli/errorcodes" 27 "github.com/arduino/arduino-cli/cli/feedback" 28 "github.com/arduino/arduino-cloud-cli/arduino" 29 "github.com/arduino/arduino-cloud-cli/config" 30 "github.com/arduino/go-paths-helper" 31 "github.com/manifoldco/promptui" 32 "github.com/sirupsen/logrus" 33 "github.com/spf13/cobra" 34 "github.com/spf13/viper" 35 ) 36 37 type initFlags struct { 38 destDir string 39 overwrite bool 40 format string 41 } 42 43 func initInitCommand() *cobra.Command { 44 flags := &initFlags{} 45 initCommand := &cobra.Command{ 46 Use: "init", 47 Short: "Initialize a credentials file", 48 Long: "Initialize an Arduino IoT Cloud CLI credentials file", 49 Run: func(cmd *cobra.Command, args []string) { 50 if err := runInitCommand(flags); err != nil { 51 feedback.Errorf("Error during credentials init: %v", err) 52 os.Exit(errorcodes.ErrGeneric) 53 } 54 }, 55 } 56 57 initCommand.Flags().StringVar(&flags.destDir, "dest-dir", "", "Sets where to save the credentials file") 58 initCommand.Flags().BoolVar(&flags.overwrite, "overwrite", false, "Overwrite existing credentials file") 59 initCommand.Flags().StringVar(&flags.format, "file-format", "yaml", "Format of the credentials file, can be {yaml|json}") 60 61 return initCommand 62 } 63 64 func runInitCommand(flags *initFlags) error { 65 logrus.Info("Initializing credentials file") 66 67 // Get default destination directory if it's not passed 68 if flags.destDir == "" { 69 credPath, err := arduino.DataDir() 70 if err != nil { 71 return fmt.Errorf("cannot retrieve arduino default directory: %w", err) 72 } 73 // Create arduino default directory if it does not exist 74 if credPath.NotExist() { 75 if err = credPath.MkdirAll(); err != nil { 76 return fmt.Errorf("cannot create arduino default directory %s: %w", credPath, err) 77 } 78 } 79 flags.destDir = credPath.String() 80 } 81 82 // Validate format flag 83 flags.format = strings.ToLower(flags.format) 84 if flags.format != "json" && flags.format != "yaml" { 85 return fmt.Errorf("format is not valid, provide 'json' or 'yaml'") 86 } 87 88 // Check that the destination directory is valid and build the credentials file path 89 credPath, err := paths.New(flags.destDir).Abs() 90 if err != nil { 91 return fmt.Errorf("cannot retrieve absolute path of %s: %w", flags.destDir, err) 92 } 93 if !credPath.IsDir() { 94 return fmt.Errorf("%s is not a valid directory", credPath) 95 } 96 credFile := credPath.Join(config.CredentialsFilename + "." + flags.format) 97 if !flags.overwrite && credFile.Exist() { 98 return fmt.Errorf("%s already exists, use '--overwrite' to overwrite it", credFile) 99 } 100 101 // Take needed credentials starting an interactive mode 102 feedback.Print("To obtain your API credentials visit https://app.arduino.cc/api-keys") 103 id, key, org, err := paramsPrompt() 104 if err != nil { 105 return fmt.Errorf("cannot take credentials params: %w", err) 106 } 107 108 // Write the credentials file 109 newSettings := viper.New() 110 newSettings.SetConfigPermissions(os.FileMode(0600)) 111 newSettings.Set("client", id) 112 newSettings.Set("secret", key) 113 newSettings.Set("organization", org) 114 if err := newSettings.WriteConfigAs(credFile.String()); err != nil { 115 return fmt.Errorf("cannot write credentials file: %w", err) 116 } 117 118 feedback.Printf("Credentials file successfully initialized at: %s", credFile) 119 return nil 120 } 121 122 func paramsPrompt() (id, key, org string, err error) { 123 prompt := promptui.Prompt{ 124 Label: "Please enter the Client ID", 125 Validate: func(s string) error { 126 if len(s) != config.ClientIDLen { 127 return errors.New("client-id not valid") 128 } 129 return nil 130 }, 131 } 132 id, err = prompt.Run() 133 if err != nil { 134 return "", "", "", fmt.Errorf("client prompt fail: %w", err) 135 } 136 137 prompt = promptui.Prompt{ 138 Mask: '*', 139 Label: "Please enter the Client Secret", 140 Validate: func(s string) error { 141 if len(s) != config.ClientSecretLen { 142 return errors.New("client secret not valid") 143 } 144 return nil 145 }, 146 } 147 key, err = prompt.Run() 148 if err != nil { 149 return "", "", "", fmt.Errorf("client secret prompt fail: %w", err) 150 } 151 152 prompt = promptui.Prompt{ 153 Mask: '*', 154 Label: "Please enter the Organization ID - if any - Leave empty otherwise", 155 Validate: func(s string) error { 156 if len(s) != 0 && len(s) != config.OrganizationLen { 157 return errors.New("organization id not valid") 158 } 159 return nil 160 }, 161 } 162 org, err = prompt.Run() 163 if err != nil { 164 return "", "", "", fmt.Errorf("organization id prompt fail: %w", err) 165 } 166 167 return id, key, org, nil 168 }