Skip to content

Read config also from env #76

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 8 commits into from
Dec 13, 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Configuration file is supported in two different format: json and yaml. Use the

`$ arduino-cloud-cli config init --config-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.

## 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
9 changes: 2 additions & 7 deletions cli/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ import (
"github.com/spf13/viper"
)

const (
clientIDLen = 32
clientSecretLen = 64
)

var initFlags struct {
destDir string
overwrite bool
Expand Down Expand Up @@ -122,7 +117,7 @@ func paramsPrompt() (id, key string, err error) {
prompt := promptui.Prompt{
Label: "Please enter the Client ID",
Validate: func(s string) error {
if len(s) != clientIDLen {
if len(s) != config.ClientIDLen {
return errors.New("client-id not valid")
}
return nil
Expand All @@ -137,7 +132,7 @@ func paramsPrompt() (id, key string, err error) {
Mask: '*',
Label: "Please enter the Client Secret",
Validate: func(s string) error {
if len(s) != clientSecretLen {
if len(s) != config.ClientSecretLen {
return errors.New("client secret not valid")
}
return nil
Expand Down
150 changes: 136 additions & 14 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,60 +25,182 @@ import (
"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
}

// Retrieve returns the actual parameters contained in the
// configuration file, if any. Returns error if no config file is found.
// 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
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 {
return c, nil
}

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)
v.AddConfigPath(*configDir)
err = v.ReadInConfig()
if err != nil {
err = fmt.Errorf("%s: %w", "retrieving config file", err)
err = fmt.Errorf(
"config file found at %s but cannot read its content: %w",
*configDir,
err,
)
return nil, err
}

conf := &Config{}
v.Unmarshal(conf)
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
}

func searchConfigDir() (string, error) {
// 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:
// 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) {
// Search in current directory and its parents.
cwd, err := paths.Getwd()
if err != nil {
return "", err
return nil, 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() {
if path.Join(Filename+".yaml").Exist() || path.Join(Filename+".json").Exist() {
return path.String(), nil
p := path.String()
return &p, nil
}
}

// Search in arduino's default data directory.
arduino15, err := arduino.DataDir()
if err != nil {
return "", err
return nil, err
}
if arduino15.Join(Filename+".yaml").Exist() || arduino15.Join(Filename+".json").Exist() {
return arduino15.String(), nil
p := arduino15.String()
return &p, nil
}

return "", fmt.Errorf(
"didn't find config file in the current directory, its parents or in %s",
arduino15.String(),
)
// Didn't find config file in the current directory, its parents or in arduino15"
return nil, nil
}
Loading