From 87651e63f323a58803623b4ca7abeec2853be448 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 21 Dec 2021 13:38:51 +0100 Subject: [PATCH] Refactor config in credentials --- README.md | 24 +-- cli/cli.go | 4 +- .../config.go => credentials/credentials.go} | 14 +- cli/{config => credentials}/init.go | 58 +++--- command/dashboard/create.go | 2 +- command/dashboard/delete.go | 2 +- command/dashboard/extract.go | 2 +- command/dashboard/list.go | 2 +- command/device/create.go | 2 +- command/device/creategeneric.go | 2 +- command/device/createlora.go | 2 +- command/device/delete.go | 2 +- command/device/list.go | 2 +- command/device/listfrequency.go | 4 +- command/ota/massupload.go | 2 +- command/ota/upload.go | 2 +- command/tag/create.go | 2 +- command/tag/delete.go | 2 +- command/thing/bind.go | 2 +- command/thing/clone.go | 2 +- command/thing/create.go | 2 +- command/thing/delete.go | 2 +- command/thing/extract.go | 2 +- command/thing/list.go | 2 +- internal/config/config.go | 171 +--------------- internal/config/credentials.go | 184 ++++++++++++++++++ .../{config_test.go => credentials_test.go} | 72 +++---- internal/config/default.go | 32 --- 28 files changed, 301 insertions(+), 300 deletions(-) rename cli/{config/config.go => credentials/credentials.go} (78%) rename cli/{config => credentials}/init.go (64%) create mode 100644 internal/config/credentials.go rename internal/config/{config_test.go => credentials_test.go} (69%) delete mode 100644 internal/config/default.go diff --git a/README.md b/README.md index 8b4d9a63..37492a71 100644 --- a/README.md +++ b/README.md @@ -28,32 +28,32 @@ Another example: let's say that the execution of the previous command results in `$ arduino-cloud-cli device create --name mydevice -v` -## Set a configuration +## Set credentials -arduino-cloud-cli needs a configuration file containing an Arduino IoT Cloud client ID and its corresponding secret. +arduino-cloud-cli needs a credentials file containing an Arduino IoT Cloud client ID and its corresponding secret. You can retrieve these credentials from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key. -Once you have the credentials, execute the following command and provide them when asked to create a new configuration file: +Once you have the credentials, execute the following command and provide them: -`$ arduino-cloud-cli config init` +`$ arduino-cloud-cli credentials init` By default it will be created in the arduino data directory (arduino15). You can specify a different destination folder with the `--dest-dir` option. -arduino-cloud-cli looks for its configuration file in different directories in the following order: current working directory, parents of the current working directory, arduino15 default directory. +arduino-cloud-cli looks for its credentials file in different directories in the following order: current working directory, parents of the current working directory, arduino15 default directory. -This gives you the possibility to use different configuration files depending on the project you are working on. +This gives you the possibility to use different credentials files depending on the project you are working on. -`$ arduino-cloud-cli config init --dest-dir ` +`$ arduino-cloud-cli credentials init --dest-dir ` -To reset an old configuration file, just overwrite it using this command: +To reset an old credentials file, just overwrite it using this command: -`$ arduino-cloud-cli config init --overwrite` +`$ arduino-cloud-cli credentials init --overwrite` -Configuration file is supported in two different format: json and yaml. Use the `--config-format` to choose it. Default is yaml. +Credentials file is supported in two different format: json and yaml. Use the `--file-format` to choose it. Default is yaml. -`$ arduino-cloud-cli config init --config-format json` +`$ arduino-cloud-cli credentials init --file-format json` -It is also possible to specify credentials directly in `ARDUINO_CLOUD_CLIENT` and `ARDUINO_CLOUD_SECRET` environment variables. Credentials specified in environment variables have higher priority than the ones specified in config files. +It is also possible to specify credentials directly in `ARDUINO_CLOUD_CLIENT` and `ARDUINO_CLOUD_SECRET` environment variables. Credentials specified in environment variables have higher priority than the ones specified in credentials files. ## Device provisioning diff --git a/cli/cli.go b/cli/cli.go index b26ed8b0..a4a6ff92 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,7 +25,7 @@ import ( "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" - "github.com/arduino/arduino-cloud-cli/cli/config" + "github.com/arduino/arduino-cloud-cli/cli/credentials" "github.com/arduino/arduino-cloud-cli/cli/dashboard" "github.com/arduino/arduino-cloud-cli/cli/device" "github.com/arduino/arduino-cloud-cli/cli/ota" @@ -49,7 +49,7 @@ func Execute() { } cli.AddCommand(version.NewCommand()) - cli.AddCommand(config.NewCommand()) + cli.AddCommand(credentials.NewCommand()) cli.AddCommand(device.NewCommand()) cli.AddCommand(thing.NewCommand()) cli.AddCommand(dashboard.NewCommand()) diff --git a/cli/config/config.go b/cli/credentials/credentials.go similarity index 78% rename from cli/config/config.go rename to cli/credentials/credentials.go index e425dc66..43e9baad 100644 --- a/cli/config/config.go +++ b/cli/credentials/credentials.go @@ -15,20 +15,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package config +package credentials import ( "github.com/spf13/cobra" ) func NewCommand() *cobra.Command { - configCommand := &cobra.Command{ - Use: "config", - Short: "Configuration commands.", - Long: "Configuration commands.", + credentialsCommand := &cobra.Command{ + Use: "credentials", + Short: "Credentials commands.", + Long: "Credentials commands.", } - configCommand.AddCommand(initInitCommand()) + credentialsCommand.AddCommand(initInitCommand()) - return configCommand + return credentialsCommand } diff --git a/cli/config/init.go b/cli/credentials/init.go similarity index 64% rename from cli/config/init.go rename to cli/credentials/init.go index 6717e5bf..28ac765b 100644 --- a/cli/config/init.go +++ b/cli/credentials/init.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package config +package credentials import ( "errors" @@ -43,81 +43,81 @@ var initFlags struct { func initInitCommand() *cobra.Command { initCommand := &cobra.Command{ Use: "init", - Short: "Initialize a configuration file", - Long: "Initialize an Arduino IoT Cloud CLI configuration", + Short: "Initialize a credentials file", + Long: "Initialize an Arduino IoT Cloud CLI credentials file", Run: runInitCommand, } - initCommand.Flags().StringVar(&initFlags.destDir, "dest-dir", "", "Sets where to save the configuration file") - initCommand.Flags().BoolVar(&initFlags.overwrite, "overwrite", false, "Overwrite existing config file") - initCommand.Flags().StringVar(&initFlags.format, "config-format", "yaml", "Format of the configuration file, can be {yaml|json}") + initCommand.Flags().StringVar(&initFlags.destDir, "dest-dir", "", "Sets where to save the credentials file") + initCommand.Flags().BoolVar(&initFlags.overwrite, "overwrite", false, "Overwrite existing credentials file") + initCommand.Flags().StringVar(&initFlags.format, "file-format", "yaml", "Format of the credentials file, can be {yaml|json}") return initCommand } func runInitCommand(cmd *cobra.Command, args []string) { - logrus.Info("Initializing config file") + logrus.Info("Initializing credentials file") // Get default destination directory if it's not passed if initFlags.destDir == "" { - configPath, err := arduino.DataDir() + credPath, err := arduino.DataDir() if err != nil { - feedback.Errorf("Error during config init: cannot retrieve arduino default directory: %v", err) + feedback.Errorf("Error during credentials init: cannot retrieve arduino default directory: %v", err) os.Exit(errorcodes.ErrGeneric) } // Create arduino default directory if it does not exist - if configPath.NotExist() { - if err = configPath.MkdirAll(); err != nil { - feedback.Errorf("Error during config init: cannot create arduino default directory %s: %v", configPath, err) + if credPath.NotExist() { + if err = credPath.MkdirAll(); err != nil { + feedback.Errorf("Error during credentials init: cannot create arduino default directory %s: %v", credPath, err) os.Exit(errorcodes.ErrGeneric) } } - initFlags.destDir = configPath.String() + initFlags.destDir = credPath.String() } // Validate format flag initFlags.format = strings.ToLower(initFlags.format) if initFlags.format != "json" && initFlags.format != "yaml" { - feedback.Error("Error during config init: format is not valid, provide 'json' or 'yaml'") + feedback.Error("Error during credentials init: format is not valid, provide 'json' or 'yaml'") os.Exit(errorcodes.ErrGeneric) } - // Check that the destination directory is valid and build the configuration file path - configPath, err := paths.New(initFlags.destDir).Abs() + // Check that the destination directory is valid and build the credentials file path + credPath, err := paths.New(initFlags.destDir).Abs() if err != nil { - feedback.Errorf("Error during config init: cannot retrieve absolute path of %s: %v", initFlags.destDir, err) + feedback.Errorf("Error during credentials init: cannot retrieve absolute path of %s: %v", initFlags.destDir, err) os.Exit(errorcodes.ErrGeneric) } - if !configPath.IsDir() { - feedback.Errorf("Error during config init: %s is not a valid directory", configPath) + if !credPath.IsDir() { + feedback.Errorf("Error during credentials init: %s is not a valid directory", credPath) os.Exit(errorcodes.ErrGeneric) } - configFile := configPath.Join(config.Filename + "." + initFlags.format) - if !initFlags.overwrite && configFile.Exist() { - feedback.Errorf("Error during config init: %s already exists, use '--overwrite' to overwrite it", - configFile) + credFile := credPath.Join(config.CredentialsFilename + "." + initFlags.format) + if !initFlags.overwrite && credFile.Exist() { + feedback.Errorf("Error during credentials init: %s already exists, use '--overwrite' to overwrite it", + credFile) os.Exit(errorcodes.ErrGeneric) } - // Take needed configuration parameters starting an interactive mode + // Take needed credentials starting an interactive mode feedback.Print("To obtain your API credentials visit https://create.arduino.cc/iot/integrations") id, key, err := paramsPrompt() if err != nil { - feedback.Errorf("Error during config init: cannot take config params: %v", err) + feedback.Errorf("Error during credentials init: cannot take credentials params: %v", err) os.Exit(errorcodes.ErrGeneric) } - // Write the configuration file + // Write the credentials file newSettings := viper.New() newSettings.SetConfigPermissions(os.FileMode(0600)) newSettings.Set("client", id) newSettings.Set("secret", key) - if err := newSettings.WriteConfigAs(configFile.String()); err != nil { - feedback.Errorf("Error during config init: cannot create config file: %v", err) + if err := newSettings.WriteConfigAs(credFile.String()); err != nil { + feedback.Errorf("Error during credentials init: cannot write credentials file: %v", err) os.Exit(errorcodes.ErrGeneric) } - feedback.Printf("Config file successfully initialized at: %s", configFile) + feedback.Printf("Credentials file successfully initialized at: %s", credFile) } func paramsPrompt() (id, key string, err error) { diff --git a/command/dashboard/create.go b/command/dashboard/create.go index f26e350c..17ad1fe2 100644 --- a/command/dashboard/create.go +++ b/command/dashboard/create.go @@ -34,7 +34,7 @@ type CreateParams struct { // Create allows to create a new dashboard. func Create(params *CreateParams) (*DashboardInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/dashboard/delete.go b/command/dashboard/delete.go index 4e62374a..f4dc7069 100644 --- a/command/dashboard/delete.go +++ b/command/dashboard/delete.go @@ -31,7 +31,7 @@ type DeleteParams struct { // Delete command is used to delete a dashboard // from Arduino IoT Cloud. func Delete(params *DeleteParams) error { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/dashboard/extract.go b/command/dashboard/extract.go index a02b737b..b1a3f38c 100644 --- a/command/dashboard/extract.go +++ b/command/dashboard/extract.go @@ -34,7 +34,7 @@ type ExtractParams struct { // Extract command is used to extract a dashboard template // from a dashboard on Arduino IoT Cloud. func Extract(params *ExtractParams) (map[string]interface{}, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/dashboard/list.go b/command/dashboard/list.go index 5e9aeb43..aa737e73 100644 --- a/command/dashboard/list.go +++ b/command/dashboard/list.go @@ -25,7 +25,7 @@ import ( // List command is used to list // the dashboards of Arduino IoT Cloud. func List() ([]DashboardInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/device/create.go b/command/device/create.go index ced973f9..38f6209c 100644 --- a/command/device/create.go +++ b/command/device/create.go @@ -63,7 +63,7 @@ func Create(params *CreateParams) (*DeviceInfo, error) { ) } - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/device/creategeneric.go b/command/device/creategeneric.go index 1dae831a..0ed59b72 100644 --- a/command/device/creategeneric.go +++ b/command/device/creategeneric.go @@ -44,7 +44,7 @@ type DeviceGenericInfo struct { // CreateGeneric command is used to add a new generic device to Arduino IoT Cloud. func CreateGeneric(params *CreateGenericParams) (*DeviceGenericInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/device/createlora.go b/command/device/createlora.go index ae074720..f78e019c 100644 --- a/command/device/createlora.go +++ b/command/device/createlora.go @@ -107,7 +107,7 @@ func CreateLora(params *CreateLoraParams) (*DeviceLoraInfo, error) { return nil, err } - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/device/delete.go b/command/device/delete.go index 1c9dde92..e48953d4 100644 --- a/command/device/delete.go +++ b/command/device/delete.go @@ -43,7 +43,7 @@ func Delete(params *DeleteParams) error { return errors.New("cannot use both ID and Tags. only one of them should be not nil") } - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/device/list.go b/command/device/list.go index 9ace969e..cfb396f7 100644 --- a/command/device/list.go +++ b/command/device/list.go @@ -33,7 +33,7 @@ type ListParams struct { // List command is used to list // the devices of Arduino IoT Cloud. func List(params *ListParams) ([]DeviceInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/device/listfrequency.go b/command/device/listfrequency.go index 8f92973e..234f211a 100644 --- a/command/device/listfrequency.go +++ b/command/device/listfrequency.go @@ -34,7 +34,7 @@ type FrequencyPlanInfo struct { // ListFrequencyPlans command is used to list // the supported LoRa frequency plans. func ListFrequencyPlans() ([]FrequencyPlanInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } @@ -53,7 +53,7 @@ func ListFrequencyPlans() ([]FrequencyPlanInfo, error) { freq := FrequencyPlanInfo{ Name: f.Name, ID: f.Id, - Advanced: fmt.Sprintf("%v",f.Advanced), + Advanced: fmt.Sprintf("%v", f.Advanced), } freqs = append(freqs, freq) } diff --git a/command/ota/massupload.go b/command/ota/massupload.go index 9410d4d9..a9000765 100644 --- a/command/ota/massupload.go +++ b/command/ota/massupload.go @@ -71,7 +71,7 @@ func MassUpload(params *MassUploadParams) ([]Result, error) { return nil, fmt.Errorf("%s: %w", "cannot generate .ota file", err) } - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/ota/upload.go b/command/ota/upload.go index f10f2ba9..13294d91 100644 --- a/command/ota/upload.go +++ b/command/ota/upload.go @@ -45,7 +45,7 @@ type UploadParams struct { // Upload command is used to upload a firmware OTA, // on a device of Arduino IoT Cloud. func Upload(params *UploadParams) error { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/tag/create.go b/command/tag/create.go index fe237069..61b86021 100644 --- a/command/tag/create.go +++ b/command/tag/create.go @@ -35,7 +35,7 @@ type CreateTagsParams struct { // CreateTags allows to create or overwrite tags // on a resource of Arduino IoT Cloud. func CreateTags(params *CreateTagsParams) error { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/tag/delete.go b/command/tag/delete.go index cb8fce63..62e19ba6 100644 --- a/command/tag/delete.go +++ b/command/tag/delete.go @@ -35,7 +35,7 @@ type DeleteTagsParams struct { // DeleteTags command is used to delete tags of a device // from Arduino IoT Cloud. func DeleteTags(params *DeleteTagsParams) error { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/thing/bind.go b/command/thing/bind.go index 8f15d6ee..f854ef19 100644 --- a/command/thing/bind.go +++ b/command/thing/bind.go @@ -33,7 +33,7 @@ type BindParams struct { // Bind command is used to bind a thing to a device // on Arduino IoT Cloud. func Bind(params *BindParams) error { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/thing/clone.go b/command/thing/clone.go index 0a9f6600..c6db7640 100644 --- a/command/thing/clone.go +++ b/command/thing/clone.go @@ -33,7 +33,7 @@ type CloneParams struct { // Clone allows to create a new thing from an already existing one. func Clone(params *CloneParams) (*ThingInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/thing/create.go b/command/thing/create.go index f95eb1a0..fe679e98 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -34,7 +34,7 @@ type CreateParams struct { // Create allows to create a new thing. func Create(params *CreateParams) (*ThingInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/thing/delete.go b/command/thing/delete.go index fb5bb3df..68d24805 100644 --- a/command/thing/delete.go +++ b/command/thing/delete.go @@ -43,7 +43,7 @@ func Delete(params *DeleteParams) error { return errors.New("cannot use both ID and Tags. only one of them should be not nil") } - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return err } diff --git a/command/thing/extract.go b/command/thing/extract.go index fa3fe308..7f1ee4e1 100644 --- a/command/thing/extract.go +++ b/command/thing/extract.go @@ -34,7 +34,7 @@ type ExtractParams struct { // Extract command is used to extract a thing template // from a thing on Arduino IoT Cloud. func Extract(params *ExtractParams) (map[string]interface{}, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/command/thing/list.go b/command/thing/list.go index 9fc4a95b..548116c9 100644 --- a/command/thing/list.go +++ b/command/thing/list.go @@ -36,7 +36,7 @@ type ListParams struct { // List command is used to list // the things of Arduino IoT Cloud. func List(params *ListParams) ([]ThingInfo, error) { - conf, err := config.Retrieve() + conf, err := config.RetrieveCredentials() if err != nil { return nil, err } diff --git a/internal/config/config.go b/internal/config/config.go index 9f958432..e8c7c552 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,169 +18,18 @@ package config import ( - "fmt" - "github.com/arduino/arduino-cloud-cli/arduino" "github.com/arduino/go-paths-helper" "github.com/sirupsen/logrus" "github.com/spf13/viper" ) -const ( - // ClientIDLen specifies the length of Arduino IoT Cloud client ids. - ClientIDLen = 32 - // ClientSecretLen specifies the length of Arduino IoT Cloud client secrets. - ClientSecretLen = 64 - - // EnvPrefix is the prefix environment variables should have to be - // fetched as config parameters during the config retrieval. - EnvPrefix = "ARDUINO_CLOUD" -) - -// Config contains all the configuration parameters -// known by arduino-cloud-cli. -type Config struct { - Client string `map-structure:"client"` // Client ID of the user - Secret string `map-structure:"secret"` // Secret ID of the user, unique for each Client ID -} - -// Validate the config. -// If config is not valid, it returns an error explaining the reason. -func (c *Config) Validate() error { - if len(c.Client) != ClientIDLen { - return fmt.Errorf( - "client id not valid, expected len %d but got %d", - ClientIDLen, - len(c.Client), - ) - } - if len(c.Secret) != ClientSecretLen { - return fmt.Errorf( - "client secret not valid, expected len %d but got %d", - ClientSecretLen, - len(c.Secret), - ) - } - return nil -} - -// IsEmpty checks if config has no params set. -func (c *Config) IsEmpty() bool { - return len(c.Client) == 0 && len(c.Secret) == 0 -} - -// Retrieve looks for configuration parameters in -// environment variables or in configuration file. -// Returns error if no config is found. -func Retrieve() (*Config, error) { - // Config extracted from environment has highest priority - logrus.Info("Looking for configuration in environment variables") - c, err := fromEnv() - if err != nil { - return nil, fmt.Errorf("reading config from environment variables: %w", err) - } - // Return the config only if it has been found - if c != nil { - logrus.Info("Configuration found in environment variables") - return c, nil - } - - logrus.Info("Looking for configuration in file system") - c, err = fromFile() - if err != nil { - return nil, fmt.Errorf("reading config from file: %w", err) - } - if c != nil { - return c, nil - } - - return nil, fmt.Errorf( - "config has not been found neither in environment variables " + - "nor in the current directory, its parents or in arduino15", - ) -} - -// fromFile looks for a configuration file. -// If a config file is not found, it returns a nil config without raising errors. -// If invalid config file is found, it returns an error. -func fromFile() (*Config, error) { - // Looks for a configuration file - configDir, err := searchConfigDir() - if err != nil { - return nil, fmt.Errorf("can't get config directory: %w", err) - } - // Return nil config if no config file is found - if configDir == nil { - return nil, nil - } - - v := viper.New() - v.SetConfigName(Filename) - v.AddConfigPath(*configDir) - err = v.ReadInConfig() - if err != nil { - err = fmt.Errorf( - "config file found at %s but cannot read its content: %w", - *configDir, - err, - ) - return nil, err - } - - conf := &Config{} - err = v.Unmarshal(conf) - if err != nil { - return nil, fmt.Errorf( - "config file found at %s but cannot unmarshal it: %w", - *configDir, - err, - ) - } - if err = conf.Validate(); err != nil { - return nil, fmt.Errorf( - "config file found at %s but is not valid: %w", - *configDir, - err, - ) - } - return conf, nil -} - -// fromEnv looks for configuration credentials in environment variables. -// If credentials are not found, it returns a nil config without raising errors. -// If invalid credentials are found, it returns an error. -func fromEnv() (*Config, error) { - v := viper.New() - SetDefaults(v) - v.SetEnvPrefix(EnvPrefix) - v.AutomaticEnv() - - conf := &Config{} - err := v.Unmarshal(conf) - if err != nil { - return nil, fmt.Errorf("cannot unmarshal config from environment variables: %w", err) - } - - if conf.IsEmpty() { - return nil, nil - } - - if err = conf.Validate(); err != nil { - return nil, fmt.Errorf( - "config retrieved from environment variables with prefix '%s' are not valid: %w", - EnvPrefix, - err, - ) - } - return conf, nil -} - -// searchConfigDir configuration file in different directories in the following order: +// searchConfigDir looks for a configuration file in different directories in the following order: // current working directory, parents of the current working directory, arduino15 default directory. // Returns a nil string if no config file has been found, without raising errors. // Returns an error if any problem is encountered during the file research which prevents // to understand whether a config file exists or not. -func searchConfigDir() (*string, error) { +func searchConfigDir(confname string) (*string, error) { // Search in current directory and its parents. cwd, err := paths.Getwd() if err != nil { @@ -189,9 +38,9 @@ func searchConfigDir() (*string, error) { // Don't let bad naming mislead you, cwd.Parents()[0] is cwd itself so // we look in the current directory first and then on its parents. for _, path := range cwd.Parents() { - logrus.Infof("Looking for configuration in %s", path) - if file, found := configFileInDir(path); found { - logrus.Infof("Configuration found at %s", file) + logrus.Infof("Looking for %s in %s", confname, path) + if file, found := configFileInDir(confname, path); found { + logrus.Infof("Found %s at %s", confname, file) p := path.String() return &p, nil } @@ -202,9 +51,9 @@ func searchConfigDir() (*string, error) { if err != nil { return nil, err } - logrus.Infof("Looking for configuration in %s", arduino15) - if file, found := configFileInDir(arduino15); found { - logrus.Infof("Configuration found at %s", file) + logrus.Infof("Looking for %s in %s", confname, arduino15) + if file, found := configFileInDir(confname, arduino15); found { + logrus.Infof("%s found at %s", confname, file) p := arduino15.String() return &p, nil } @@ -217,9 +66,9 @@ func searchConfigDir() (*string, error) { // If a configuration file is found, then it is returned. // In case of multiple config files, it returns the one with the highest priority // according to viper. -func configFileInDir(dir *paths.Path) (filepath *paths.Path, found bool) { +func configFileInDir(confname string, dir *paths.Path) (filepath *paths.Path, found bool) { for _, ext := range viper.SupportedExts { - if filepath = dir.Join(Filename + "." + ext); filepath.Exist() { + if filepath = dir.Join(confname + "." + ext); filepath.Exist() { return filepath, true } } diff --git a/internal/config/credentials.go b/internal/config/credentials.go new file mode 100644 index 00000000..7d6a287c --- /dev/null +++ b/internal/config/credentials.go @@ -0,0 +1,184 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "fmt" + + "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +const ( + // ClientIDLen specifies the length of Arduino IoT Cloud client ids. + ClientIDLen = 32 + // ClientSecretLen specifies the length of Arduino IoT Cloud client secrets. + ClientSecretLen = 64 + + // EnvPrefix is the prefix environment variables should have to be + // fetched as credentials parameters during the credentials retrieval. + EnvPrefix = "ARDUINO_CLOUD" + + // CredentialsFilename specifies the name of the credentials file. + CredentialsFilename = "arduino-cloud-credentials" +) + +// SetDefaultCredentials sets the default credentials values. +func SetDefaultCredentials(settings *viper.Viper) { + // Client ID + settings.SetDefault("client", "") + // Secret + settings.SetDefault("secret", "") +} + +// Credentials contains the parameters of Arduino IoT Cloud credentials. +type Credentials struct { + Client string `map-structure:"client"` // Client ID of the user + Secret string `map-structure:"secret"` // Secret ID of the user, unique for each Client ID +} + +// Validate the credentials. +// If credentials are not valid, it returns an error explaining the reason. +func (c *Credentials) Validate() error { + if len(c.Client) != ClientIDLen { + return fmt.Errorf( + "client id not valid, expected len %d but got %d", + ClientIDLen, + len(c.Client), + ) + } + if len(c.Secret) != ClientSecretLen { + return fmt.Errorf( + "client secret not valid, expected len %d but got %d", + ClientSecretLen, + len(c.Secret), + ) + } + return nil +} + +// IsEmpty checks if credentials has no params set. +func (c *Credentials) IsEmpty() bool { + return len(c.Client) == 0 && len(c.Secret) == 0 +} + +// RetrieveCredentials looks for credentials in +// environment variables or in credentials file. +// Returns error if no credentials are found. +func RetrieveCredentials() (*Credentials, error) { + // Credentials extracted from environment has highest priority + logrus.Info("Looking for credentials in environment variables") + c, err := fromEnv() + if err != nil { + return nil, fmt.Errorf("reading credentials from environment variables: %w", err) + } + // Return credentials only if found + if c != nil { + logrus.Info("Credentials found in environment variables") + return c, nil + } + + logrus.Info("Looking for credentials in file system") + c, err = fromFile() + if err != nil { + return nil, fmt.Errorf("reading credentials from file: %w", err) + } + if c != nil { + return c, nil + } + + return nil, fmt.Errorf( + "credentials have not been found neither in environment variables " + + "nor in the current directory, its parents or in arduino15", + ) +} + +// fromFile looks for a credentials file. +// If a credentials file is not found, it returns nil credentials without raising errors. +// If invalid credentials file is found, it returns an error. +func fromFile() (*Credentials, error) { + // Looks for a credentials file + configDir, err := searchConfigDir(CredentialsFilename) + if err != nil { + return nil, fmt.Errorf("can't get credentials directory: %w", err) + } + // Return nil credentials if no config file is found + if configDir == nil { + return nil, nil + } + + v := viper.New() + v.SetConfigName(CredentialsFilename) + v.AddConfigPath(*configDir) + err = v.ReadInConfig() + if err != nil { + err = fmt.Errorf( + "credentials file found at %s but cannot read its content: %w", + *configDir, + err, + ) + return nil, err + } + + cred := &Credentials{} + err = v.Unmarshal(cred) + if err != nil { + return nil, fmt.Errorf( + "credentials file found at %s but cannot unmarshal it: %w", + *configDir, + err, + ) + } + if err = cred.Validate(); err != nil { + return nil, fmt.Errorf( + "credentials file found at %s but is not valid: %w", + *configDir, + err, + ) + } + return cred, nil +} + +// fromEnv looks for credentials in environment variables. +// If credentials are not found, it returns nil credentials without raising errors. +// If invalid credentials are found, it returns an error. +func fromEnv() (*Credentials, error) { + v := viper.New() + SetDefaultCredentials(v) + v.SetEnvPrefix(EnvPrefix) + v.AutomaticEnv() + + cred := &Credentials{} + err := v.Unmarshal(cred) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal credentials from environment variables: %w", err) + } + + if cred.IsEmpty() { + return nil, nil + } + + if err = cred.Validate(); err != nil { + return nil, fmt.Errorf( + "credentials retrieved from environment variables with prefix '%s' are not valid: %w", + EnvPrefix, + err, + ) + } + return cred, nil +} diff --git a/internal/config/config_test.go b/internal/config/credentials_test.go similarity index 69% rename from internal/config/config_test.go rename to internal/config/credentials_test.go index 2fb7b47f..41b3d1a0 100644 --- a/internal/config/config_test.go +++ b/internal/config/credentials_test.go @@ -26,12 +26,12 @@ import ( "github.com/google/go-cmp/cmp" ) -func TestRetrieve(t *testing.T) { +func TestRetrieveCredentials(t *testing.T) { var ( validSecret = "qaRZGEbnQNNvmaeTLqy8Bxs22wLZ6H7obIiNSveTLPdoQuylANnuy6WBOw16XoqH" validClient = "CQ4iZ5sebOfhGRwUn3IV0r1YFMNrMTIx" - validConfig = &Config{validClient, validSecret} - invalidConfig = &Config{"", validSecret} + validConfig = &Credentials{validClient, validSecret} + invalidConfig = &Credentials{"", validSecret} clientEnv = EnvPrefix + "_CLIENT" secretEnv = EnvPrefix + "_SECRET" ) @@ -40,11 +40,11 @@ func TestRetrieve(t *testing.T) { name string pre func() post func() - wantedConfig *Config + wantedConfig *Credentials wantedErr bool }{ { - name: "valid config written in env", + name: "valid credentials written in env", pre: func() { os.Setenv(clientEnv, validConfig.Client) os.Setenv(secretEnv, validConfig.Secret) @@ -58,7 +58,7 @@ func TestRetrieve(t *testing.T) { }, { - name: "invalid config written in env", + name: "invalid credentials written in env", pre: func() { os.Setenv(clientEnv, validConfig.Client) os.Setenv(secretEnv, "") @@ -72,16 +72,16 @@ func TestRetrieve(t *testing.T) { }, { - name: "valid config written in parent of cwd", + name: "valid credentials written in parent of cwd", pre: func() { parent := "test-parent" cwd := "test-parent/test-cwd" os.MkdirAll(cwd, os.FileMode(0777)) - // Write valid config in parent dir + // Write valid credentials in parent dir os.Chdir(parent) b, _ := json.Marshal(validConfig) - os.WriteFile(Filename+".json", b, os.FileMode(0777)) - // Cwd has no config file + os.WriteFile(CredentialsFilename+".json", b, os.FileMode(0777)) + // Cwd has no credentials file os.Chdir("test-cwd") }, post: func() { @@ -93,19 +93,19 @@ func TestRetrieve(t *testing.T) { }, { - name: "invalid config written in cwd, ignore config of parent dir", + name: "invalid credentials written in cwd, ignore credentials of parent dir", pre: func() { parent := "test-parent" cwd := "test-parent/test-cwd" os.MkdirAll(cwd, os.FileMode(0777)) - // Write valid config in parent dir + // Write valid credentials in parent dir os.Chdir(parent) b, _ := json.Marshal(validConfig) - os.WriteFile(Filename+".json", b, os.FileMode(0777)) - // Write invalid config in cwd + os.WriteFile(CredentialsFilename+".json", b, os.FileMode(0777)) + // Write invalid credentials in cwd os.Chdir("test-cwd") b, _ = json.Marshal(invalidConfig) - os.WriteFile(Filename+".json", b, os.FileMode(0777)) + os.WriteFile(CredentialsFilename+".json", b, os.FileMode(0777)) }, post: func() { os.Chdir("../..") @@ -118,15 +118,15 @@ func TestRetrieve(t *testing.T) { }, { - name: "invalid config written in env, ignore valid config of cwd", + name: "invalid credentials written in env, ignore valid credentials of cwd", pre: func() { cwd := "test-cwd" os.MkdirAll(cwd, os.FileMode(0777)) - // Write valid config in cwd + // Write valid credentials in cwd os.Chdir(cwd) b, _ := json.Marshal(validConfig) - os.WriteFile(Filename+".json", b, os.FileMode(0777)) - // Write invalid config in env + os.WriteFile(CredentialsFilename+".json", b, os.FileMode(0777)) + // Write invalid credentials in env os.Setenv(clientEnv, validConfig.Client) os.Setenv(secretEnv, "") }, @@ -142,7 +142,7 @@ func TestRetrieve(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.pre() - got, err := Retrieve() + got, err := RetrieveCredentials() tt.post() if tt.wantedErr && err == nil { @@ -153,7 +153,7 @@ func TestRetrieve(t *testing.T) { } if !cmp.Equal(got, tt.wantedConfig) { - t.Errorf("Wrong config received, diff:\n%s", cmp.Diff(tt.wantedConfig, got)) + t.Errorf("Wrong credentials received, diff:\n%s", cmp.Diff(tt.wantedConfig, got)) } }) } @@ -166,12 +166,12 @@ func TestValidate(t *testing.T) { ) tests := []struct { name string - config *Config + config *Credentials valid bool }{ { - name: "valid config", - config: &Config{ + name: "valid credentials", + config: &Credentials{ Client: validClient, Secret: validSecret, }, @@ -179,12 +179,12 @@ func TestValidate(t *testing.T) { }, { name: "invalid client id", - config: &Config{Client: "", Secret: validSecret}, + config: &Credentials{Client: "", Secret: validSecret}, valid: false, }, { name: "invalid client secret", - config: &Config{Client: validClient, Secret: ""}, + config: &Credentials{Client: validClient, Secret: ""}, valid: false, }, } @@ -194,14 +194,14 @@ func TestValidate(t *testing.T) { err := tt.config.Validate() if tt.valid && err != nil { t.Errorf( - "Wrong validation, the config was correct but an error was received: \nconfig: %v\nerr: %v", + "Wrong validation, the credentials were correct but an error was received: \ncredentials: %v\nerr: %v", tt.config, err, ) } if !tt.valid && err == nil { t.Errorf( - "Wrong validation, the config was invalid but no error was received: \nconfig: %v", + "Wrong validation, the credentials were invalid but no error was received: \ncredentials: %v", tt.config, ) } @@ -216,22 +216,22 @@ func TestIsEmpty(t *testing.T) { ) tests := []struct { name string - config *Config + config *Credentials want bool }{ { - name: "empty config", - config: &Config{Client: "", Secret: ""}, + name: "empty credentials", + config: &Credentials{Client: "", Secret: ""}, want: true, }, { - name: "config without id", - config: &Config{Client: "", Secret: validSecret}, + name: "credentials without id", + config: &Credentials{Client: "", Secret: validSecret}, want: false, }, { - name: "config without secret", - config: &Config{Client: validClient, Secret: ""}, + name: "credentials without secret", + config: &Credentials{Client: validClient, Secret: ""}, want: false, }, } @@ -240,7 +240,7 @@ func TestIsEmpty(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got := tt.config.IsEmpty() if got != tt.want { - t.Errorf("Expected %v but got %v, with config: %v", tt.want, got, tt.config) + t.Errorf("Expected %v but got %v, with credentials: %v", tt.want, got, tt.config) } }) } diff --git a/internal/config/default.go b/internal/config/default.go deleted file mode 100644 index d593d317..00000000 --- a/internal/config/default.go +++ /dev/null @@ -1,32 +0,0 @@ -// This file is part of arduino-cloud-cli. -// -// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package config - -import "github.com/spf13/viper" - -var ( - Filename = "arduino-cloud" -) - -// SetDefaults sets the default values for configuration keys. -func SetDefaults(settings *viper.Viper) { - // Client ID - settings.SetDefault("client", "") - // Secret - settings.SetDefault("secret", "") -}