Skip to content

Add credentials find command #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Credentials file is supported in two different format: json and yaml. Use the `-

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.

#### Find credentials

To have information about which credentials would be used in the current folder you can use the following command:

`$ arduino-cloud-cli credentials find`

## Device provisioning

When provisioning a device, you can optionally specify the port to which the device is connected to and its fqbn. If they are not given, then the first device found will be provisioned.
Expand Down
1 change: 1 addition & 0 deletions cli/credentials/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func NewCommand() *cobra.Command {
}

credentialsCommand.AddCommand(initInitCommand())
credentialsCommand.AddCommand(initFindCommand())

return credentialsCommand
}
51 changes: 51 additions & 0 deletions cli/credentials/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 <https://www.gnu.org/licenses/>.

package credentials

import (
"os"

"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cloud-cli/internal/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func initFindCommand() *cobra.Command {
findCommand := &cobra.Command{
Use: "find",
Short: "Find the credentials that would be used in your current directory",
Long: "Find the credentials to access Arduino IoT Cloud that would be used in your current directory",
Run: runFindCommand,
}

return findCommand
}

func runFindCommand(cmd *cobra.Command, args []string) {
logrus.Info("Looking for credentials")

src, err := config.FindCredentials()
if err != nil {
feedback.Errorf("Error during credentials find: %v", err)
os.Exit(errorcodes.ErrGeneric)
}

feedback.Printf("Using credentials in: %s", src)
}
18 changes: 8 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,40 @@ import (
"github.com/spf13/viper"
)

// searchConfigDir looks for a configuration file in different directories in the following order:
// searchConfigFile 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 empty string and false 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(confname string) (*string, error) {
func searchConfigFile(confname string) (dir string, found bool, err error) {
// Search in current directory and its parents.
cwd, err := paths.Getwd()
if err != nil {
return nil, err
return "", false, err
}
// 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 %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
return file.String(), true, nil
}
}

// Search in arduino's default data directory.
arduino15, err := arduino.DataDir()
if err != nil {
return nil, err
return "", false, err
}
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
return file.String(), true, nil
}

// Didn't find config file in the current directory, its parents or in arduino15"
return nil, nil
return "", false, nil
}

// configFileInDir looks for a configuration file in the passed directory.
Expand Down
133 changes: 72 additions & 61 deletions internal/config/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package config

import (
"fmt"
"strings"

"github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
Expand All @@ -38,8 +40,8 @@ const (
CredentialsFilename = "arduino-cloud-credentials"
)

// SetDefaultCredentials sets the default credentials values.
func SetDefaultCredentials(settings *viper.Viper) {
// SetEmptyCredentials sets the default credentials values to empty strings.
func SetEmptyCredentials(settings *viper.Viper) {
// Client ID
settings.SetDefault("client", "")
// Secret
Expand Down Expand Up @@ -77,89 +79,110 @@ func (c *Credentials) IsEmpty() bool {
return len(c.Client) == 0 && len(c.Secret) == 0
}

// RetrieveCredentials looks for credentials in
// FindCredentials looks for credentials in
// environment variables or in credentials file.
// Returns error if no credentials are found.
func RetrieveCredentials() (*Credentials, error) {
// Returns the source of found credentials (env or filepath).
// Returns an error if credentials are not found
// specifying paths where the credentials are searched.
func FindCredentials() (source string, err 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 "", fmt.Errorf("looking for credentials in environment variables: %w", err)
}
// Return credentials only if found
if c != nil {
logrus.Info("Credentials found in environment variables")
return c, nil
if !c.IsEmpty() {
logrus.Infof("Credentials found in environment variables with prefix '%s'", EnvPrefix)
return "environment variables", nil
}

logrus.Info("Looking for credentials in file system")
c, err = fromFile()
path, found, err := searchConfigFile(CredentialsFilename)
if err != nil {
return nil, fmt.Errorf("reading credentials from file: %w", err)
return "", fmt.Errorf("looking for credentials files: %w", err)
}
if c != nil {
return c, nil
if found {
return path, nil
}

return nil, fmt.Errorf(
return "", 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)
// RetrieveCredentials retrieves credentials from
// environment variables or credentials file.
// Returns error if credentials are not found or
// if found credentials are invalid.
func RetrieveCredentials() (cred *Credentials, err error) {
// Credentials extracted from environment has highest priority
logrus.Info("Looking for credentials in environment variables")
cred, err = fromEnv()
if err != nil {
return nil, fmt.Errorf("reading credentials from environment variables: %w", err)
}
// Returns credentials if found in env
if !cred.IsEmpty() {
// Returns error if credentials are found but are not valid
if err := cred.Validate(); err != nil {
return nil, fmt.Errorf(
"credentials retrieved from environment variables with prefix '%s' are not valid: %w", EnvPrefix, err,
)
}
logrus.Infof("Credentials found in environment variables with prefix '%s'", EnvPrefix)
return cred, nil
}

logrus.Info("Looking for credentials in file system")
filepath, found, err := searchConfigFile(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
// Returns credentials if found in a file
if found {
if cred, err = fromFile(filepath); err != nil {
return nil, fmt.Errorf("reading credentials from file %s: %w", filepath, err)
}
// Returns error if credentials are found but are not valid
if err := cred.Validate(); err != nil {
return nil, fmt.Errorf(
"credentials retrieved from file %s are not valid: %w", filepath, err,
)
}
return cred, 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 retrieves credentials from a credentials file.
// Returns error if credentials are not found or cannot be fetched.
func fromFile(filepath string) (*Credentials, error) {
v := viper.New()
v.SetConfigName(CredentialsFilename)
v.AddConfigPath(*configDir)
err = v.ReadInConfig()
v.SetConfigFile(filepath)
v.SetConfigType(strings.TrimLeft(paths.New(filepath).Ext(), "."))
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
return nil, fmt.Errorf("cannot read credentials file: %w", 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 nil, fmt.Errorf("cannot unmarshal credentials file: %w", 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.
// fromEnv retrieves credentials from environment variables.
// Returns empty credentials if not found.
func fromEnv() (*Credentials, error) {
v := viper.New()
SetDefaultCredentials(v)
SetEmptyCredentials(v)
v.SetEnvPrefix(EnvPrefix)
v.AutomaticEnv()

Expand All @@ -168,17 +191,5 @@ func fromEnv() (*Credentials, error) {
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
}