From 112c28033da9487d53734b916a4d13da957e6f75 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Thu, 25 Jun 2020 12:04:42 +0200 Subject: [PATCH] Provisioning: Apply settings expanders (#25692) * Provisioning: Apply settings expanders Adds support for the variable expanders introduced to the configuration file in #25075 to provisioning. * Update pkg/services/provisioning/values/values_test.go Co-authored-by: Arve Knudsen * Update pkg/services/provisioning/values/values.go Co-authored-by: Arve Knudsen * Provisioning: Handle errors when expanding JSON maps --- pkg/services/provisioning/values/values.go | 56 +++++++++++++----- .../provisioning/values/values_test.go | 59 +++++++++++++++++++ 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/pkg/services/provisioning/values/values.go b/pkg/services/provisioning/values/values.go index deb1f0617b6..d2f890894e4 100644 --- a/pkg/services/provisioning/values/values.go +++ b/pkg/services/provisioning/values/values.go @@ -11,11 +11,14 @@ package values import ( + "fmt" "os" "reflect" "strconv" "strings" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" ) @@ -117,7 +120,10 @@ func (val *JSONValue) UnmarshalYAML(unmarshal func(interface{}) error) error { interpolated := make(map[string]interface{}) raw := make(map[string]interface{}) for key, val := range unmarshaled { - interpolated[key], raw[key] = transformInterface(val) + interpolated[key], raw[key], err = transformInterface(val) + if err != nil { + return err + } } val.Raw = raw @@ -143,7 +149,10 @@ func (val *StringMapValue) UnmarshalYAML(unmarshal func(interface{}) error) erro interpolated := make(map[string]string) raw := make(map[string]string) for key, val := range unmarshaled { - interpolated[key], raw[key] = interpolateValue(val) + interpolated[key], raw[key], err = interpolateValue(val) + if err != nil { + return err + } } val.Raw = raw val.value = interpolated @@ -158,11 +167,11 @@ func (val *StringMapValue) Value() map[string]string { // slices and the actual interpolation is done on all simple string values in the structure. It returns a copy of any // map or slice value instead of modifying them in place and also return value without interpolation but with converted // type as a second value. -func transformInterface(i interface{}) (interface{}, interface{}) { +func transformInterface(i interface{}) (interface{}, interface{}, error) { typeOf := reflect.TypeOf(i) if typeOf == nil { - return nil, nil + return nil, nil, nil } switch typeOf.Kind() { @@ -174,43 +183,55 @@ func transformInterface(i interface{}) (interface{}, interface{}) { return interpolateValue(i.(string)) default: // Was int, float or some other value that we do not need to do any transform on. - return i, i + return i, i, nil } } -func transformSlice(i []interface{}) (interface{}, interface{}) { +func transformSlice(i []interface{}) (interface{}, interface{}, error) { var transformedSlice []interface{} var rawSlice []interface{} for _, val := range i { - transformed, raw := transformInterface(val) + transformed, raw, err := transformInterface(val) + if err != nil { + return nil, nil, err + } transformedSlice = append(transformedSlice, transformed) rawSlice = append(rawSlice, raw) } - return transformedSlice, rawSlice + return transformedSlice, rawSlice, nil } -func transformMap(i map[interface{}]interface{}) (interface{}, interface{}) { +func transformMap(i map[interface{}]interface{}) (interface{}, interface{}, error) { transformed := make(map[string]interface{}) raw := make(map[string]interface{}) for key, val := range i { stringKey, ok := key.(string) if ok { - transformed[stringKey], raw[stringKey] = transformInterface(val) + var err error + transformed[stringKey], raw[stringKey], err = transformInterface(val) + if err != nil { + return nil, nil, err + } } } - return transformed, raw + return transformed, raw, nil } -// interpolateValue returns final value after interpolation. At the moment only env var interpolation is done -// here but in the future something like interpolation from file could be also done here. +// interpolateValue returns the final value after interpolation. In addition to environment variable interpolation, +// expanders available for the settings file are expanded here. // For a literal '$', '$$' can be used to avoid interpolation. -func interpolateValue(val string) (string, string) { +func interpolateValue(val string) (string, string, error) { parts := strings.Split(val, "$$") interpolated := make([]string, len(parts)) for i, v := range parts { + expanded, err := setting.ExpandVar(v) + if err != nil { + return val, val, fmt.Errorf("failed to interpolate value '%s': %w", val, err) + } + v = expanded interpolated[i] = os.ExpandEnv(v) } - return strings.Join(interpolated, "$"), val + return strings.Join(interpolated, "$"), val, nil } type interpolated struct { @@ -228,6 +249,9 @@ func getInterpolated(unmarshal func(interface{}) error) (*interpolated, error) { } // We get new raw value here which can have a bit different type, as yaml types nested maps as // map[interface{}]interface and we want it to be map[string]interface{} - value, raw := interpolateValue(veryRaw) + value, raw, err := interpolateValue(veryRaw) + if err != nil { + return &interpolated{}, err + } return &interpolated{raw: raw, value: value}, nil } diff --git a/pkg/services/provisioning/values/values_test.go b/pkg/services/provisioning/values/values_test.go index 2b255323a65..bc5dd9f7809 100644 --- a/pkg/services/provisioning/values/values_test.go +++ b/pkg/services/provisioning/values/values_test.go @@ -1,9 +1,18 @@ package values import ( + "errors" + "fmt" + "io/ioutil" "os" "testing" + "github.com/grafana/grafana/pkg/setting" + "gopkg.in/ini.v1" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + . "github.com/smartystreets/goconvey/convey" "gopkg.in/yaml.v2" ) @@ -245,3 +254,53 @@ func unmarshalingTest(document string, out interface{}) { err := yaml.Unmarshal([]byte(document), out) So(err, ShouldBeNil) } + +func TestValues_readFile(t *testing.T) { + type Data struct { + Val StringValue `yaml:"val"` + } + + f, err := ioutil.TempFile(os.TempDir(), "file expansion *") + require.NoError(t, err) + file := f.Name() + + defer func() { + require.NoError(t, os.Remove(file)) + }() + + const expected = "hello, world" + _, err = f.WriteString(expected) + require.NoError(t, err) + require.NoError(t, f.Close()) + + data := &Data{} + err = yaml.Unmarshal([]byte(fmt.Sprintf("val: $__file{%s}", file)), data) + require.NoError(t, err) + assert.Equal(t, expected, data.Val.Value()) +} + +func TestValues_expanderError(t *testing.T) { + type Data struct { + Top JSONValue `yaml:"top"` + } + + setting.AddExpander("fail", 0, failExpander{}) + + data := &Data{} + err := yaml.Unmarshal([]byte("top:\n val: $__fail{val}"), data) + require.Error(t, err) + require.Truef(t, errors.Is(err, errExpand), "expected error to wrap: %v\ngot: %v", errExpand, err) + assert.Empty(t, data) +} + +var errExpand = errors.New("test error: bad expander") + +type failExpander struct{} + +func (f failExpander) SetupExpander(file *ini.File) error { + return nil +} + +func (f failExpander) Expand(s string) (string, error) { + return "", errExpand +}