mirror of https://github.com/grafana/grafana
Settings: Expand variables in configuration (#25075)
parent
55f304f15d
commit
e8b5f2330d
@ -0,0 +1,147 @@ |
||||
package setting |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"regexp" |
||||
"sort" |
||||
"strings" |
||||
|
||||
"gopkg.in/ini.v1" |
||||
) |
||||
|
||||
type Expander interface { |
||||
SetupExpander(file *ini.File) error |
||||
Expand(string) (string, error) |
||||
} |
||||
|
||||
type registeredExpander struct { |
||||
name string |
||||
priority int64 |
||||
expander Expander |
||||
} |
||||
|
||||
var expanders = []registeredExpander{ |
||||
{ |
||||
name: "env", |
||||
priority: -10, |
||||
expander: envExpander{}, |
||||
}, |
||||
{ |
||||
name: "file", |
||||
priority: -5, |
||||
expander: fileExpander{}, |
||||
}, |
||||
} |
||||
|
||||
func AddExpander(name string, priority int64, e Expander) { |
||||
expanders = append(expanders, registeredExpander{ |
||||
name: name, |
||||
priority: priority, |
||||
expander: e, |
||||
}) |
||||
} |
||||
|
||||
var regex = regexp.MustCompile(`\$(|__\w+){([^}]+)}`) |
||||
|
||||
func expandConfig(file *ini.File) error { |
||||
sort.Slice(expanders, func(i, j int) bool { |
||||
return expanders[i].priority < expanders[j].priority |
||||
}) |
||||
|
||||
for _, expander := range expanders { |
||||
err := expander.expander.SetupExpander(file) |
||||
if err != nil { |
||||
return fmt.Errorf("got error during initilazation of expander '%s': %w", expander.name, err) |
||||
} |
||||
|
||||
for _, section := range file.Sections() { |
||||
for _, key := range section.Keys() { |
||||
updated, err := applyExpander(key.Value(), expander) |
||||
if err != nil { |
||||
return fmt.Errorf("got error while expanding %s.%s with expander '%s': %w", |
||||
section.Name(), |
||||
key.Name(), |
||||
expander.name, |
||||
err) |
||||
} |
||||
|
||||
key.SetValue(updated) |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func ExpandVar(s string) (string, error) { |
||||
for _, expander := range expanders { |
||||
var err error |
||||
s, err = applyExpander(s, expander) |
||||
if err != nil { |
||||
return "", fmt.Errorf("got error while expanding expander %s: %w", expander.name, err) |
||||
} |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
func applyExpander(s string, e registeredExpander) (string, error) { |
||||
matches := regex.FindAllStringSubmatch(s, -1) |
||||
|
||||
for _, match := range matches { |
||||
if len(match) < 3 { |
||||
return "", fmt.Errorf("regex error, got %d results back for match, expected 3", len(match)) |
||||
} |
||||
|
||||
_, isEnv := e.expander.(envExpander) |
||||
if match[1] == "__"+e.name || (match[1] == "" && isEnv) { |
||||
updated, err := e.expander.Expand(match[2]) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
s = strings.Replace(s, match[0], updated, 1) |
||||
} |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
type envExpander struct { |
||||
} |
||||
|
||||
func (e envExpander) SetupExpander(file *ini.File) error { |
||||
return nil |
||||
} |
||||
|
||||
func (e envExpander) Expand(s string) (string, error) { |
||||
envValue := os.Getenv(s) |
||||
|
||||
// if env variable is hostname and it is empty use os.Hostname as default
|
||||
if s == "HOSTNAME" && envValue == "" { |
||||
return os.Hostname() |
||||
} |
||||
|
||||
return os.Getenv(s), nil |
||||
} |
||||
|
||||
type fileExpander struct { |
||||
} |
||||
|
||||
func (e fileExpander) SetupExpander(file *ini.File) error { |
||||
return nil |
||||
} |
||||
|
||||
func (e fileExpander) Expand(s string) (string, error) { |
||||
_, err := os.Stat(s) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
f, err := ioutil.ReadFile(s) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return strings.TrimSpace(string(f)), nil |
||||
} |
||||
@ -0,0 +1,96 @@ |
||||
package setting |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"math/rand" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestExpandVar_EnvSuccessful(t *testing.T) { |
||||
const key = "GF_TEST_SETTING_EXPANDER_ENV" |
||||
const expected = "aurora borealis" |
||||
os.Setenv(key, expected) |
||||
|
||||
// expanded format
|
||||
{ |
||||
got, err := ExpandVar(fmt.Sprintf("$__env{%s}", key)) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, expected, got) |
||||
} |
||||
|
||||
// short format
|
||||
{ |
||||
got, err := ExpandVar(fmt.Sprintf("${%s}", key)) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, expected, got) |
||||
} |
||||
} |
||||
|
||||
func TestExpandVar_FileSuccessful(t *testing.T) { |
||||
f, err := ioutil.TempFile(os.TempDir(), "file expansion *") |
||||
require.NoError(t, err) |
||||
file := f.Name() |
||||
|
||||
defer func() { |
||||
os.Remove(file) |
||||
}() |
||||
|
||||
_, err = f.WriteString("hello, world") |
||||
require.NoError(t, err) |
||||
f.Close() |
||||
|
||||
got, err := ExpandVar(fmt.Sprintf("$__file{%s}", file)) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "hello, world", got) |
||||
} |
||||
|
||||
func TestExpandVar_FileDoesNotExist(t *testing.T) { |
||||
got, err := ExpandVar( |
||||
fmt.Sprintf("$__file{%s%sthisisnotarealfile_%d}", |
||||
os.TempDir(), |
||||
string(os.PathSeparator), |
||||
rand.Int63()), |
||||
) |
||||
assert.Error(t, err) |
||||
assert.True(t, errors.Is(err, os.ErrNotExist)) |
||||
assert.Empty(t, got) |
||||
} |
||||
|
||||
func TestExpanderRegex(t *testing.T) { |
||||
tests := map[string][][]string{ |
||||
// we should not expand variables where there are none
|
||||
"smoketest": {}, |
||||
"Pa$$word{0}": {}, |
||||
"$_almost{but not quite a variable}": {}, |
||||
// args are required
|
||||
"$__file{}": {}, |
||||
|
||||
// base cases
|
||||
"${ENV}": {{"", "ENV"}}, |
||||
"$__env{ENV}": {{"__env", "ENV"}}, |
||||
"$__file{/dev/null}": {{"__file", "/dev/null"}}, |
||||
"$__vault{item}": {{"__vault", "item"}}, |
||||
// contains a space in the argument
|
||||
"$__file{C:\\Program Files\\grafana\\something}": {{"__file", "C:\\Program Files\\grafana\\something"}}, |
||||
|
||||
// complex cases
|
||||
"get variable from $__env{ENV}ironment": {{"__env", "ENV"}}, |
||||
"$__env{VAR1} $__file{/var/lib/grafana/secrets/var1}": {{"__env", "VAR1"}, {"__file", "/var/lib/grafana/secrets/var1"}}, |
||||
"$__env{$__file{this is invalid}}": {{"__env", "$__file{this is invalid"}}, |
||||
} |
||||
|
||||
for input, expected := range tests { |
||||
output := regex.FindAllStringSubmatch(input, -1) |
||||
require.Len(t, output, len(expected)) |
||||
for i, variable := range output { |
||||
assert.Equal(t, expected[i], variable[1:]) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue