Skip to content

Commit 8b8895c

Browse files
committed
Tolerate errors in invalid config files, and report them as warnings
1 parent 0cc60bb commit 8b8895c

File tree

7 files changed

+155
-44
lines changed

7 files changed

+155
-44
lines changed

commands/service_settings.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222
"reflect"
2323

2424
"github.com/arduino/arduino-cli/commands/cmderrors"
25+
f "github.com/arduino/arduino-cli/internal/algorithms"
2526
"github.com/arduino/arduino-cli/internal/cli/configuration"
27+
"github.com/arduino/arduino-cli/internal/go-configmap"
2628
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2729
"google.golang.org/protobuf/proto"
2830
"gopkg.in/yaml.v3"
@@ -187,23 +189,29 @@ func (s *arduinoCoreServerImpl) ConfigurationSave(ctx context.Context, req *rpc.
187189

188190
// SettingsReadFromFile read settings from a YAML file and replace the settings currently stored in memory.
189191
func (s *arduinoCoreServerImpl) ConfigurationOpen(ctx context.Context, req *rpc.ConfigurationOpenRequest) (*rpc.ConfigurationOpenResponse, error) {
192+
warnings := []string{}
193+
190194
switch req.GetSettingsFormat() {
191195
case "yaml":
192196
err := yaml.Unmarshal([]byte(req.GetEncodedSettings()), s.settings)
193-
if err != nil {
197+
if errs, ok := err.(*configmap.UnmarshalErrors); ok {
198+
warnings = f.Map(errs.WrappedErrors(), (error).Error)
199+
} else if err != nil {
194200
return nil, fmt.Errorf("error unmarshalling settings: %v", err)
195201
}
196202
case "json":
197203
err := json.Unmarshal([]byte(req.GetEncodedSettings()), s.settings)
198-
if err != nil {
204+
if errs, ok := err.(*configmap.UnmarshalErrors); ok {
205+
warnings = f.Map(errs.WrappedErrors(), (error).Error)
206+
} else if err != nil {
199207
return nil, fmt.Errorf("error unmarshalling settings: %v", err)
200208
}
201209
default:
202210
return nil, &cmderrors.InvalidArgumentError{Message: fmt.Sprintf("unsupported format: %s", req.GetSettingsFormat())}
203211
}
204212

205213
configuration.BindEnvVars(s.settings)
206-
return &rpc.ConfigurationOpenResponse{}, nil
214+
return &rpc.ConfigurationOpenResponse{Warnings: warnings}, nil
207215
}
208216

209217
// SettingsEnumerate returns the list of all the settings keys.

internal/go-configmap/errors.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2024 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package configmap
17+
18+
import "strings"
19+
20+
// UnmarshalErrors is a collection of errors that occurred during unmarshalling.
21+
// Do not return this type directly, but use its result() method instead.
22+
type UnmarshalErrors struct {
23+
wrapped []error
24+
}
25+
26+
func (e *UnmarshalErrors) append(err error) {
27+
e.wrapped = append(e.wrapped, err)
28+
}
29+
30+
func (e *UnmarshalErrors) result() error {
31+
if len(e.wrapped) == 0 {
32+
return nil
33+
}
34+
return e
35+
}
36+
37+
func (e *UnmarshalErrors) Error() string {
38+
if len(e.wrapped) == 1 {
39+
return e.wrapped[0].Error()
40+
}
41+
errs := []string{"multiple errors:"}
42+
for _, err := range e.wrapped {
43+
errs = append(errs, "- "+err.Error())
44+
}
45+
return strings.Join(errs, "\n")
46+
}
47+
48+
// WrappedErrors returns the list of errors that occurred during unmarshalling.
49+
func (e *UnmarshalErrors) WrappedErrors() []error {
50+
if e == nil {
51+
return nil
52+
}
53+
return e.wrapped
54+
}

internal/go-configmap/yaml.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ func (c *Map) UnmarshalYAML(node *yaml.Node) error {
2929
return err
3030
}
3131

32+
errs := &UnmarshalErrors{}
3233
for k, v := range flattenMap(in) {
3334
if err := c.Set(k, v); err != nil {
34-
return err
35+
errs.append(err)
3536
}
3637
}
37-
return nil
38+
return errs.result()
3839
}

internal/integrationtest/config/config_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package config_test
1717

1818
import (
1919
"path/filepath"
20+
"strings"
2021
"testing"
2122

2223
"github.com/arduino/arduino-cli/internal/integrationtest"
@@ -867,3 +868,29 @@ func TestInitializationOrderOfConfigThroughFlagAndEnv(t *testing.T) {
867868
require.NoError(t, err)
868869
requirejson.Contains(t, stdout, `{"config":{ "locale": "test" }}`)
869870
}
871+
872+
func TestConfigLoadWithUnknownOrInvalidKeys(t *testing.T) {
873+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
874+
defer env.CleanUp()
875+
876+
tmp := t.TempDir()
877+
unkwnownConfig := paths.New(filepath.Join(tmp, "unknown.yaml"))
878+
unkwnownConfig.WriteFile([]byte(`
879+
unk: "test"
880+
build.unk: 123
881+
`))
882+
t.Cleanup(func() { unkwnownConfig.Remove() })
883+
884+
// Run "config get" with a configuration containing an unknown key
885+
out, _, err := cli.Run("config", "get", "locale", "--config-file", unkwnownConfig.String())
886+
require.NoError(t, err)
887+
require.Equal(t, "en", strings.TrimSpace(string(out)))
888+
889+
// Run "config get" with a configuration containing an invalid key
890+
invalidConfig := paths.New(filepath.Join(tmp, "invalid.yaml"))
891+
invalidConfig.WriteFile([]byte(`locale: 123`))
892+
t.Cleanup(func() { invalidConfig.Remove() })
893+
out, _, err = cli.Run("config", "get", "locale", "--config-file", invalidConfig.String())
894+
require.NoError(t, err)
895+
require.Equal(t, "en", strings.TrimSpace(string(out)))
896+
}

main.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ func main() {
4545
} else if !os.IsNotExist(err) {
4646
feedback.FatalError(fmt.Errorf("couldn't read configuration file: %w", err), feedback.ErrGeneric)
4747
}
48-
if _, err := srv.ConfigurationOpen(ctx, openReq); err != nil {
48+
if resp, err := srv.ConfigurationOpen(ctx, openReq); err != nil {
4949
feedback.FatalError(fmt.Errorf("couldn't load configuration: %w", err), feedback.ErrGeneric)
50+
} else if warnings := resp.GetWarnings(); len(warnings) > 0 {
51+
for _, warning := range warnings {
52+
feedback.Warning(warning)
53+
}
5054
}
5155

5256
// Get the current settings from the server

rpc/cc/arduino/cli/commands/v1/settings.pb.go

+50-37
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rpc/cc/arduino/cli/commands/v1/settings.proto

+5-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ message ConfigurationOpenRequest {
120120
string settings_format = 2;
121121
}
122122

123-
message ConfigurationOpenResponse {}
123+
message ConfigurationOpenResponse {
124+
// Warnings that occurred while opening the configuration (e.g. unknown keys,
125+
// or invalid values)
126+
repeated string warnings = 1;
127+
}
124128

125129
message SettingsGetValueRequest {
126130
// The key to get

0 commit comments

Comments
 (0)