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  }