Skip to content

Commit 696a58f

Browse files
author
Paolo Calao
authored
Fetch config from env (#76)
Allows the cli to fetch configuration credentials from environment variables. Config credentials can now be specified directly in `ARDUINO_CLOUD_CLIENT` and `ARDUINO_CLOUD_SECRET` environment variables. The config is now searched in this way: - if credentials are found on environment variables, return them - otherwise look for them in config files - if not found anywhere return an error
1 parent 851f1bf commit 696a58f

File tree

6 files changed

+396
-24
lines changed

6 files changed

+396
-24
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Configuration file is supported in two different format: json and yaml. Use the
5353

5454
`$ arduino-cloud-cli config init --config-format json`
5555

56+
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.
57+
5658
## Device provisioning
5759

5860
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.

cli/config/init.go

+2-7
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ import (
3434
"github.com/spf13/viper"
3535
)
3636

37-
const (
38-
clientIDLen = 32
39-
clientSecretLen = 64
40-
)
41-
4237
var initFlags struct {
4338
destDir string
4439
overwrite bool
@@ -122,7 +117,7 @@ func paramsPrompt() (id, key string, err error) {
122117
prompt := promptui.Prompt{
123118
Label: "Please enter the Client ID",
124119
Validate: func(s string) error {
125-
if len(s) != clientIDLen {
120+
if len(s) != config.ClientIDLen {
126121
return errors.New("client-id not valid")
127122
}
128123
return nil
@@ -137,7 +132,7 @@ func paramsPrompt() (id, key string, err error) {
137132
Mask: '*',
138133
Label: "Please enter the Client Secret",
139134
Validate: func(s string) error {
140-
if len(s) != clientSecretLen {
135+
if len(s) != config.ClientSecretLen {
141136
return errors.New("client secret not valid")
142137
}
143138
return nil

internal/config/config.go

+136-14
Original file line numberDiff line numberDiff line change
@@ -25,60 +25,182 @@ import (
2525
"github.com/spf13/viper"
2626
)
2727

28+
const (
29+
// ClientIDLen specifies the length of Arduino IoT Cloud client ids.
30+
ClientIDLen = 32
31+
// ClientSecretLen specifies the length of Arduino IoT Cloud client secrets.
32+
ClientSecretLen = 64
33+
34+
// EnvPrefix is the prefix environment variables should have to be
35+
// fetched as config parameters during the config retrieval.
36+
EnvPrefix = "ARDUINO_CLOUD"
37+
)
38+
2839
// Config contains all the configuration parameters
2940
// known by arduino-cloud-cli.
3041
type Config struct {
3142
Client string `map-structure:"client"` // Client ID of the user
3243
Secret string `map-structure:"secret"` // Secret ID of the user, unique for each Client ID
3344
}
3445

35-
// Retrieve returns the actual parameters contained in the
36-
// configuration file, if any. Returns error if no config file is found.
46+
// Validate the config.
47+
// If config is not valid, it returns an error explaining the reason.
48+
func (c *Config) Validate() error {
49+
if len(c.Client) != ClientIDLen {
50+
return fmt.Errorf(
51+
"client id not valid, expected len %d but got %d",
52+
ClientIDLen,
53+
len(c.Client),
54+
)
55+
}
56+
if len(c.Secret) != ClientSecretLen {
57+
return fmt.Errorf(
58+
"client secret not valid, expected len %d but got %d",
59+
ClientSecretLen,
60+
len(c.Secret),
61+
)
62+
}
63+
return nil
64+
}
65+
66+
// IsEmpty checks if config has no params set.
67+
func (c *Config) IsEmpty() bool {
68+
return len(c.Client) == 0 && len(c.Secret) == 0
69+
}
70+
71+
// Retrieve looks for configuration parameters in
72+
// environment variables or in configuration file.
73+
// Returns error if no config is found.
3774
func Retrieve() (*Config, error) {
75+
// Config extracted from environment has highest priority
76+
c, err := fromEnv()
77+
if err != nil {
78+
return nil, fmt.Errorf("reading config from environment variables: %w", err)
79+
}
80+
// Return the config only if it has been found
81+
if c != nil {
82+
return c, nil
83+
}
84+
85+
c, err = fromFile()
86+
if err != nil {
87+
return nil, fmt.Errorf("reading config from file: %w", err)
88+
}
89+
if c != nil {
90+
return c, nil
91+
}
92+
93+
return nil, fmt.Errorf(
94+
"config has not been found neither in environment variables " +
95+
"nor in the current directory, its parents or in arduino15",
96+
)
97+
}
98+
99+
// fromFile looks for a configuration file.
100+
// If a config file is not found, it returns a nil config without raising errors.
101+
// If invalid config file is found, it returns an error.
102+
func fromFile() (*Config, error) {
103+
// Looks for a configuration file
38104
configDir, err := searchConfigDir()
39105
if err != nil {
40106
return nil, fmt.Errorf("can't get config directory: %w", err)
41107
}
108+
// Return nil config if no config file is found
109+
if configDir == nil {
110+
return nil, nil
111+
}
42112

43113
v := viper.New()
44114
v.SetConfigName(Filename)
45-
v.AddConfigPath(configDir)
115+
v.AddConfigPath(*configDir)
46116
err = v.ReadInConfig()
47117
if err != nil {
48-
err = fmt.Errorf("%s: %w", "retrieving config file", err)
118+
err = fmt.Errorf(
119+
"config file found at %s but cannot read its content: %w",
120+
*configDir,
121+
err,
122+
)
49123
return nil, err
50124
}
51125

52126
conf := &Config{}
53-
v.Unmarshal(conf)
127+
err = v.Unmarshal(conf)
128+
if err != nil {
129+
return nil, fmt.Errorf(
130+
"config file found at %s but cannot unmarshal it: %w",
131+
*configDir,
132+
err,
133+
)
134+
}
135+
if err = conf.Validate(); err != nil {
136+
return nil, fmt.Errorf(
137+
"config file found at %s but is not valid: %w",
138+
*configDir,
139+
err,
140+
)
141+
}
54142
return conf, nil
55143
}
56144

57-
func searchConfigDir() (string, error) {
145+
// fromEnv looks for configuration credentials in environment variables.
146+
// If credentials are not found, it returns a nil config without raising errors.
147+
// If invalid credentials are found, it returns an error.
148+
func fromEnv() (*Config, error) {
149+
v := viper.New()
150+
SetDefaults(v)
151+
v.SetEnvPrefix(EnvPrefix)
152+
v.AutomaticEnv()
153+
154+
conf := &Config{}
155+
err := v.Unmarshal(conf)
156+
if err != nil {
157+
return nil, fmt.Errorf("cannot unmarshal config from environment variables: %w", err)
158+
}
159+
160+
if conf.IsEmpty() {
161+
return nil, nil
162+
}
163+
164+
if err = conf.Validate(); err != nil {
165+
return nil, fmt.Errorf(
166+
"config retrieved from environment variables with prefix '%s' are not valid: %w",
167+
EnvPrefix,
168+
err,
169+
)
170+
}
171+
return conf, nil
172+
}
173+
174+
// searchConfigDir configuration file in different directories in the following order:
175+
// current working directory, parents of the current working directory, arduino15 default directory.
176+
// Returns a nil string if no config file has been found, without raising errors.
177+
// Returns an error if any problem is encountered during the file research which prevents
178+
// to understand whether a config file exists or not.
179+
func searchConfigDir() (*string, error) {
58180
// Search in current directory and its parents.
59181
cwd, err := paths.Getwd()
60182
if err != nil {
61-
return "", err
183+
return nil, err
62184
}
63185
// Don't let bad naming mislead you, cwd.Parents()[0] is cwd itself so
64186
// we look in the current directory first and then on its parents.
65187
for _, path := range cwd.Parents() {
66188
if path.Join(Filename+".yaml").Exist() || path.Join(Filename+".json").Exist() {
67-
return path.String(), nil
189+
p := path.String()
190+
return &p, nil
68191
}
69192
}
70193

71194
// Search in arduino's default data directory.
72195
arduino15, err := arduino.DataDir()
73196
if err != nil {
74-
return "", err
197+
return nil, err
75198
}
76199
if arduino15.Join(Filename+".yaml").Exist() || arduino15.Join(Filename+".json").Exist() {
77-
return arduino15.String(), nil
200+
p := arduino15.String()
201+
return &p, nil
78202
}
79203

80-
return "", fmt.Errorf(
81-
"didn't find config file in the current directory, its parents or in %s",
82-
arduino15.String(),
83-
)
204+
// Didn't find config file in the current directory, its parents or in arduino15"
205+
return nil, nil
84206
}

0 commit comments

Comments
 (0)