Commit fcebd713 by Andrej Ocenas Committed by GitHub

Provisioning: Interpolate env vars in provisioning files (#16499)

* Add value types with custom unmarshalling logic

* Add env support for notifications config

* Use env vars in json data tests for values

* Add some more complexities to value tests

* Update comment with example usage

* Set env directly in the tests, removing patching

* Update documentation

* Add env var to the file reader tests

* Add raw value

* Post merge fixes

* Add comment
parent eb8af01a
...@@ -30,33 +30,19 @@ Checkout the [configuration](/installation/configuration) page for more informat ...@@ -30,33 +30,19 @@ Checkout the [configuration](/installation/configuration) page for more informat
### Using Environment Variables ### Using Environment Variables
All options in the configuration file (listed below) can be overridden It is possible to use environment variable interpolation in all 3 provisioning config types. Allowed syntax
using environment variables using the syntax: is either `$ENV_VAR_NAME` or `${ENV_VAR_NAME}` and can be used only for values not for keys or bigger parts
of the configs. It is not available in the dashboards definition files just the dashboard provisioning
configuration.
Example:
```bash ```yaml
GF_<SectionName>_<KeyName> datasources:
``` - name: Graphite
url: http://localhost:$PORT
Where the section name is the text within the brackets. Everything user: $USER
should be upper case and `.` should be replaced by `_`. For example, given these configuration settings: secureJsonData:
password: $PASSWORD
```bash
# default section
instance_name = ${HOSTNAME}
[security]
admin_user = admin
[auth.google]
client_secret = 0ldS3cretKey
```
Overriding will be done like so:
```bash
export GF_DEFAULT_INSTANCE_NAME=my-instance
export GF_SECURITY_ADMIN_USER=true
export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey
``` ```
<hr /> <hr />
......
package dashboards package dashboards
import ( import (
"os"
"testing" "testing"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
...@@ -18,8 +19,10 @@ func TestDashboardsAsConfig(t *testing.T) { ...@@ -18,8 +19,10 @@ func TestDashboardsAsConfig(t *testing.T) {
logger := log.New("test-logger") logger := log.New("test-logger")
Convey("Can read config file version 1 format", func() { Convey("Can read config file version 1 format", func() {
_ = os.Setenv("TEST_VAR", "general")
cfgProvider := configReader{path: simpleDashboardConfig, log: logger} cfgProvider := configReader{path: simpleDashboardConfig, log: logger}
cfg, err := cfgProvider.readConfig() cfg, err := cfgProvider.readConfig()
_ = os.Unsetenv("TEST_VAR")
So(err, ShouldBeNil) So(err, ShouldBeNil)
validateDashboardAsConfig(t, cfg) validateDashboardAsConfig(t, cfg)
......
apiVersion: 1 apiVersion: 1
providers: providers:
- name: 'general dashboards' - name: '$TEST_VAR dashboards'
orgId: 2 orgId: 2
folder: 'developers' folder: 'developers'
folderUid: 'xyz' folderUid: 'xyz'
......
package dashboards package dashboards
import ( import (
"github.com/grafana/grafana/pkg/services/provisioning/values"
"time" "time"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
...@@ -42,15 +43,15 @@ type DashboardAsConfigV1 struct { ...@@ -42,15 +43,15 @@ type DashboardAsConfigV1 struct {
} }
type DashboardProviderConfigs struct { type DashboardProviderConfigs struct {
Name string `json:"name" yaml:"name"` Name values.StringValue `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"` Type values.StringValue `json:"type" yaml:"type"`
OrgId int64 `json:"orgId" yaml:"orgId"` OrgId values.Int64Value `json:"orgId" yaml:"orgId"`
Folder string `json:"folder" yaml:"folder"` Folder values.StringValue `json:"folder" yaml:"folder"`
FolderUid string `json:"folderUid" yaml:"folderUid"` FolderUid values.StringValue `json:"folderUid" yaml:"folderUid"`
Editable bool `json:"editable" yaml:"editable"` Editable values.BoolValue `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"` Options values.JSONValue `json:"options" yaml:"options"`
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` DisableDeletion values.BoolValue `json:"disableDeletion" yaml:"disableDeletion"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"` UpdateIntervalSeconds values.Int64Value `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
} }
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
...@@ -94,15 +95,15 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { ...@@ -94,15 +95,15 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
for _, v := range dc.Providers { for _, v := range dc.Providers {
r = append(r, &DashboardsAsConfig{ r = append(r, &DashboardsAsConfig{
Name: v.Name, Name: v.Name.Value(),
Type: v.Type, Type: v.Type.Value(),
OrgId: v.OrgId, OrgId: v.OrgId.Value(),
Folder: v.Folder, Folder: v.Folder.Value(),
FolderUid: v.FolderUid, FolderUid: v.FolderUid.Value(),
Editable: v.Editable, Editable: v.Editable.Value(),
Options: v.Options, Options: v.Options.Value(),
DisableDeletion: v.DisableDeletion, DisableDeletion: v.DisableDeletion.Value(),
UpdateIntervalSeconds: v.UpdateIntervalSeconds, UpdateIntervalSeconds: v.UpdateIntervalSeconds.Value(),
}) })
} }
......
package datasources package datasources
import ( import (
"os"
"testing" "testing"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
...@@ -146,8 +147,10 @@ func TestDatasourceAsConfig(t *testing.T) { ...@@ -146,8 +147,10 @@ func TestDatasourceAsConfig(t *testing.T) {
}) })
Convey("can read all properties from version 1", func() { Convey("can read all properties from version 1", func() {
_ = os.Setenv("TEST_VAR", "name")
cfgProvifer := &configReader{log: log.New("test logger")} cfgProvifer := &configReader{log: log.New("test logger")}
cfg, err := cfgProvifer.readConfig(allProperties) cfg, err := cfgProvifer.readConfig(allProperties)
_ = os.Unsetenv("TEST_VAR")
if err != nil { if err != nil {
t.Fatalf("readConfig return an error %v", err) t.Fatalf("readConfig return an error %v", err)
} }
......
apiVersion: 1 apiVersion: 1
datasources: datasources:
- name: name - name: $TEST_VAR
type: type type: type
access: proxy access: proxy
orgId: 2 orgId: 2
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
) )
type ConfigVersion struct { type ConfigVersion struct {
...@@ -64,8 +65,8 @@ type DeleteDatasourceConfigV0 struct { ...@@ -64,8 +65,8 @@ type DeleteDatasourceConfigV0 struct {
} }
type DeleteDatasourceConfigV1 struct { type DeleteDatasourceConfigV1 struct {
OrgId int64 `json:"orgId" yaml:"orgId"` OrgId values.Int64Value `json:"orgId" yaml:"orgId"`
Name string `json:"name" yaml:"name"` Name values.StringValue `json:"name" yaml:"name"`
} }
type DataSourceFromConfigV0 struct { type DataSourceFromConfigV0 struct {
...@@ -89,23 +90,23 @@ type DataSourceFromConfigV0 struct { ...@@ -89,23 +90,23 @@ type DataSourceFromConfigV0 struct {
} }
type DataSourceFromConfigV1 struct { type DataSourceFromConfigV1 struct {
OrgId int64 `json:"orgId" yaml:"orgId"` OrgId values.Int64Value `json:"orgId" yaml:"orgId"`
Version int `json:"version" yaml:"version"` Version values.IntValue `json:"version" yaml:"version"`
Name string `json:"name" yaml:"name"` Name values.StringValue `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"` Type values.StringValue `json:"type" yaml:"type"`
Access string `json:"access" yaml:"access"` Access values.StringValue `json:"access" yaml:"access"`
Url string `json:"url" yaml:"url"` Url values.StringValue `json:"url" yaml:"url"`
Password string `json:"password" yaml:"password"` Password values.StringValue `json:"password" yaml:"password"`
User string `json:"user" yaml:"user"` User values.StringValue `json:"user" yaml:"user"`
Database string `json:"database" yaml:"database"` Database values.StringValue `json:"database" yaml:"database"`
BasicAuth bool `json:"basicAuth" yaml:"basicAuth"` BasicAuth values.BoolValue `json:"basicAuth" yaml:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser" yaml:"basicAuthUser"` BasicAuthUser values.StringValue `json:"basicAuthUser" yaml:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword" yaml:"basicAuthPassword"` BasicAuthPassword values.StringValue `json:"basicAuthPassword" yaml:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials" yaml:"withCredentials"` WithCredentials values.BoolValue `json:"withCredentials" yaml:"withCredentials"`
IsDefault bool `json:"isDefault" yaml:"isDefault"` IsDefault values.BoolValue `json:"isDefault" yaml:"isDefault"`
JsonData map[string]interface{} `json:"jsonData" yaml:"jsonData"` JsonData values.JSONValue `json:"jsonData" yaml:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData" yaml:"secureJsonData"` SecureJsonData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
Editable bool `json:"editable" yaml:"editable"` Editable values.BoolValue `json:"editable" yaml:"editable"`
} }
func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig { func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
...@@ -119,36 +120,47 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D ...@@ -119,36 +120,47 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
for _, ds := range cfg.Datasources { for _, ds := range cfg.Datasources {
r.Datasources = append(r.Datasources, &DataSourceFromConfig{ r.Datasources = append(r.Datasources, &DataSourceFromConfig{
OrgId: ds.OrgId, OrgId: ds.OrgId.Value(),
Name: ds.Name, Name: ds.Name.Value(),
Type: ds.Type, Type: ds.Type.Value(),
Access: ds.Access, Access: ds.Access.Value(),
Url: ds.Url, Url: ds.Url.Value(),
Password: ds.Password, Password: ds.Password.Value(),
User: ds.User, User: ds.User.Value(),
Database: ds.Database, Database: ds.Database.Value(),
BasicAuth: ds.BasicAuth, BasicAuth: ds.BasicAuth.Value(),
BasicAuthUser: ds.BasicAuthUser, BasicAuthUser: ds.BasicAuthUser.Value(),
BasicAuthPassword: ds.BasicAuthPassword, BasicAuthPassword: ds.BasicAuthPassword.Value(),
WithCredentials: ds.WithCredentials, WithCredentials: ds.WithCredentials.Value(),
IsDefault: ds.IsDefault, IsDefault: ds.IsDefault.Value(),
JsonData: ds.JsonData, JsonData: ds.JsonData.Value(),
SecureJsonData: ds.SecureJsonData, SecureJsonData: ds.SecureJsonData.Value(),
Editable: ds.Editable, Editable: ds.Editable.Value(),
Version: ds.Version, Version: ds.Version.Value(),
}) })
if ds.Password != "" {
cfg.log.Warn("[Deprecated] the use of password field is deprecated. Please use secureJsonData.password", "datasource name", ds.Name) // Using Raw value for the warnings here so that even if it uses env interpolation and the env var is empty
// it will still warn
if len(ds.Password.Raw) > 0 {
cfg.log.Warn(
"[Deprecated] the use of password field is deprecated. Please use secureJsonData.password",
"datasource name",
ds.Name.Value(),
)
} }
if ds.BasicAuthPassword != "" { if len(ds.BasicAuthPassword.Raw) > 0 {
cfg.log.Warn("[Deprecated] the use of basicAuthPassword field is deprecated. Please use secureJsonData.basicAuthPassword", "datasource name", ds.Name) cfg.log.Warn(
"[Deprecated] the use of basicAuthPassword field is deprecated. Please use secureJsonData.basicAuthPassword",
"datasource name",
ds.Name.Value(),
)
} }
} }
for _, ds := range cfg.DeleteDatasources { for _, ds := range cfg.DeleteDatasources {
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{ r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
OrgId: ds.OrgId, OrgId: ds.OrgId.Value(),
Name: ds.Name, Name: ds.Name.Value(),
}) })
} }
......
...@@ -131,39 +131,6 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not ...@@ -131,39 +131,6 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not
return nil return nil
} }
func (cfg *notificationsAsConfig) mapToNotificationFromConfig() *notificationsAsConfig {
r := &notificationsAsConfig{}
if cfg == nil {
return r
}
for _, notification := range cfg.Notifications {
r.Notifications = append(r.Notifications, &notificationFromConfig{
Uid: notification.Uid,
OrgId: notification.OrgId,
OrgName: notification.OrgName,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Settings: notification.Settings,
DisableResolveMessage: notification.DisableResolveMessage,
Frequency: notification.Frequency,
SendReminder: notification.SendReminder,
})
}
for _, notification := range cfg.DeleteNotifications {
r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
Uid: notification.Uid,
OrgId: notification.OrgId,
OrgName: notification.OrgName,
Name: notification.Name,
})
}
return r
}
func (dc *NotificationProvisioner) applyChanges(configPath string) error { func (dc *NotificationProvisioner) applyChanges(configPath string) error {
configs, err := dc.cfgProvider.readConfig(configPath) configs, err := dc.cfgProvider.readConfig(configPath)
if err != nil { if err != nil {
......
...@@ -63,7 +63,7 @@ func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) ( ...@@ -63,7 +63,7 @@ func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (
return nil, err return nil, err
} }
var cfg *notificationsAsConfig var cfg *notificationsAsConfigV0
err = yaml.Unmarshal(yamlFile, &cfg) err = yaml.Unmarshal(yamlFile, &cfg)
if err != nil { if err != nil {
return nil, err return nil, err
......
package notifiers package notifiers
import ( import (
"os"
"testing" "testing"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
...@@ -43,8 +44,10 @@ func TestNotificationAsConfig(t *testing.T) { ...@@ -43,8 +44,10 @@ func TestNotificationAsConfig(t *testing.T) {
}) })
Convey("Can read correct properties", func() { Convey("Can read correct properties", func() {
_ = os.Setenv("TEST_VAR", "default")
cfgProvifer := &configReader{log: log.New("test logger")} cfgProvifer := &configReader{log: log.New("test logger")}
cfg, err := cfgProvifer.readConfig(correct_properties) cfg, err := cfgProvifer.readConfig(correct_properties)
_ = os.Unsetenv("TEST_VAR")
if err != nil { if err != nil {
t.Fatalf("readConfig return an error %v", err) t.Fatalf("readConfig return an error %v", err)
} }
......
notifiers: notifiers:
- name: default-slack-notification - name: $TEST_VAR-slack-notification
type: slack type: slack
uid: notifier1 uid: notifier1
org_id: 2 org_id: 2
......
package notifiers package notifiers
import "github.com/grafana/grafana/pkg/components/simplejson" import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
// notificationsAsConfig is normalized data object for notifications config data. Any config version should be mappable
// to this type.
type notificationsAsConfig struct { type notificationsAsConfig struct {
Notifications []*notificationFromConfig `json:"notifiers" yaml:"notifiers"` Notifications []*notificationFromConfig
DeleteNotifications []*deleteNotificationConfig `json:"delete_notifiers" yaml:"delete_notifiers"` DeleteNotifications []*deleteNotificationConfig
} }
type deleteNotificationConfig struct { type deleteNotificationConfig struct {
Uid string `json:"uid" yaml:"uid"` Uid string
Name string `json:"name" yaml:"name"` Name string
OrgId int64 `json:"org_id" yaml:"org_id"` OrgId int64
OrgName string `json:"org_name" yaml:"org_name"` OrgName string
} }
type notificationFromConfig struct { type notificationFromConfig struct {
Uid string `json:"uid" yaml:"uid"` Uid string
OrgId int64 `json:"org_id" yaml:"org_id"` OrgId int64
OrgName string `json:"org_name" yaml:"org_name"` OrgName string
Name string `json:"name" yaml:"name"` Name string
Type string `json:"type" yaml:"type"` Type string
SendReminder bool `json:"send_reminder" yaml:"send_reminder"` SendReminder bool
DisableResolveMessage bool `json:"disable_resolve_message" yaml:"disable_resolve_message"` DisableResolveMessage bool
Frequency string `json:"frequency" yaml:"frequency"` Frequency string
IsDefault bool `json:"is_default" yaml:"is_default"` IsDefault bool
Settings map[string]interface{} `json:"settings" yaml:"settings"` Settings map[string]interface{}
}
// notificationsAsConfigV0 is mapping for zero version configs. This is mapped to its normalised version.
type notificationsAsConfigV0 struct {
Notifications []*notificationFromConfigV0 `json:"notifiers" yaml:"notifiers"`
DeleteNotifications []*deleteNotificationConfigV0 `json:"delete_notifiers" yaml:"delete_notifiers"`
}
type deleteNotificationConfigV0 struct {
Uid values.StringValue `json:"uid" yaml:"uid"`
Name values.StringValue `json:"name" yaml:"name"`
OrgId values.Int64Value `json:"org_id" yaml:"org_id"`
OrgName values.StringValue `json:"org_name" yaml:"org_name"`
}
type notificationFromConfigV0 struct {
Uid values.StringValue `json:"uid" yaml:"uid"`
OrgId values.Int64Value `json:"org_id" yaml:"org_id"`
OrgName values.StringValue `json:"org_name" yaml:"org_name"`
Name values.StringValue `json:"name" yaml:"name"`
Type values.StringValue `json:"type" yaml:"type"`
SendReminder values.BoolValue `json:"send_reminder" yaml:"send_reminder"`
DisableResolveMessage values.BoolValue `json:"disable_resolve_message" yaml:"disable_resolve_message"`
Frequency values.StringValue `json:"frequency" yaml:"frequency"`
IsDefault values.BoolValue `json:"is_default" yaml:"is_default"`
Settings values.JSONValue `json:"settings" yaml:"settings"`
} }
func (notification notificationFromConfig) SettingsToJson() *simplejson.Json { func (notification notificationFromConfig) SettingsToJson() *simplejson.Json {
...@@ -36,3 +67,38 @@ func (notification notificationFromConfig) SettingsToJson() *simplejson.Json { ...@@ -36,3 +67,38 @@ func (notification notificationFromConfig) SettingsToJson() *simplejson.Json {
} }
return settings return settings
} }
// mapToNotificationFromConfig maps config syntax to normalized notificationsAsConfig object. Every version
// of the config syntax should have this function.
func (cfg *notificationsAsConfigV0) mapToNotificationFromConfig() *notificationsAsConfig {
r := &notificationsAsConfig{}
if cfg == nil {
return r
}
for _, notification := range cfg.Notifications {
r.Notifications = append(r.Notifications, &notificationFromConfig{
Uid: notification.Uid.Value(),
OrgId: notification.OrgId.Value(),
OrgName: notification.OrgName.Value(),
Name: notification.Name.Value(),
Type: notification.Type.Value(),
IsDefault: notification.IsDefault.Value(),
Settings: notification.Settings.Value(),
DisableResolveMessage: notification.DisableResolveMessage.Value(),
Frequency: notification.Frequency.Value(),
SendReminder: notification.SendReminder.Value(),
})
}
for _, notification := range cfg.DeleteNotifications {
r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
Uid: notification.Uid.Value(),
OrgId: notification.OrgId.Value(),
OrgName: notification.OrgName.Value(),
Name: notification.Name.Value(),
})
}
return r
}
// A set of value types to use in provisioning. They add custom unmarshaling logic that puts the string values
// through os.ExpandEnv.
// Usage:
// type Data struct {
// Field StringValue `yaml:"field"` // Instead of string
// }
// d := &Data{}
// // unmarshal into d
// d.Field.Value() // returns the final interpolated value from the yaml file
//
package values
import (
"github.com/pkg/errors"
"os"
"reflect"
"strconv"
)
type IntValue struct {
value int
Raw string
}
func (val *IntValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
interpolated, err := getInterpolated(unmarshal)
if err != nil {
return err
}
if len(interpolated.value) == 0 {
// To keep the same behaviour as the yaml lib which just does not set the value if it is empty.
return nil
}
val.Raw = interpolated.raw
val.value, err = strconv.Atoi(interpolated.value)
return errors.Wrap(err, "cannot convert value int")
}
func (val *IntValue) Value() int {
return val.value
}
type Int64Value struct {
value int64
Raw string
}
func (val *Int64Value) UnmarshalYAML(unmarshal func(interface{}) error) error {
interpolated, err := getInterpolated(unmarshal)
if err != nil {
return err
}
if len(interpolated.value) == 0 {
// To keep the same behaviour as the yaml lib which just does not set the value if it is empty.
return nil
}
val.Raw = interpolated.raw
val.value, err = strconv.ParseInt(interpolated.value, 10, 64)
return err
}
func (val *Int64Value) Value() int64 {
return val.value
}
type StringValue struct {
value string
Raw string
}
func (val *StringValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
interpolated, err := getInterpolated(unmarshal)
if err != nil {
return err
}
val.Raw = interpolated.raw
val.value = interpolated.value
return err
}
func (val *StringValue) Value() string {
return val.value
}
type BoolValue struct {
value bool
Raw string
}
func (val *BoolValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
interpolated, err := getInterpolated(unmarshal)
if err != nil {
return err
}
val.Raw = interpolated.raw
val.value, err = strconv.ParseBool(interpolated.value)
return err
}
func (val *BoolValue) Value() bool {
return val.value
}
type JSONValue struct {
value map[string]interface{}
Raw map[string]interface{}
}
func (val *JSONValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
unmarshaled := make(map[string]interface{})
err := unmarshal(unmarshaled)
if err != nil {
return err
}
val.Raw = unmarshaled
interpolated := make(map[string]interface{})
for key, val := range unmarshaled {
interpolated[key] = tranformInterface(val)
}
val.value = interpolated
return err
}
func (val *JSONValue) Value() map[string]interface{} {
return val.value
}
type StringMapValue struct {
value map[string]string
Raw map[string]string
}
func (val *StringMapValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
unmarshaled := make(map[string]string)
err := unmarshal(unmarshaled)
if err != nil {
return err
}
val.Raw = unmarshaled
interpolated := make(map[string]string)
for key, val := range unmarshaled {
interpolated[key] = interpolateValue(val)
}
val.value = interpolated
return err
}
func (val *StringMapValue) Value() map[string]string {
return val.value
}
// tranformInterface tries to transform any interface type into proper value with env expansion. It travers maps and
// 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.
func tranformInterface(i interface{}) interface{} {
switch reflect.TypeOf(i).Kind() {
case reflect.Slice:
return transformSlice(i.([]interface{}))
case reflect.Map:
return transformMap(i.(map[interface{}]interface{}))
case reflect.String:
return interpolateValue(i.(string))
default:
// Was int, float or some other value that we do not need to do any transform on.
return i
}
}
func transformSlice(i []interface{}) interface{} {
var transformed []interface{}
for _, val := range i {
transformed = append(transformed, tranformInterface(val))
}
return transformed
}
func transformMap(i map[interface{}]interface{}) interface{} {
transformed := make(map[interface{}]interface{})
for key, val := range i {
transformed[key] = tranformInterface(val)
}
return transformed
}
// 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.
func interpolateValue(val string) string {
return os.ExpandEnv(val)
}
type interpolated struct {
value string
raw string
}
// getInterpolated unmarshals the value as string and runs interpolation on it. It is the responsibility of each
// value type to convert this string value to appropriate type.
func getInterpolated(unmarshal func(interface{}) error) (*interpolated, error) {
var raw string
err := unmarshal(&raw)
if err != nil {
return &interpolated{}, err
}
value := interpolateValue(raw)
return &interpolated{raw: raw, value: value}, nil
}
package values
import (
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/yaml.v2"
"os"
"testing"
)
func TestValues(t *testing.T) {
Convey("Values", t, func() {
os.Setenv("INT", "1")
os.Setenv("STRING", "test")
os.Setenv("BOOL", "true")
Convey("IntValue", func() {
type Data struct {
Val IntValue `yaml:"val"`
}
d := &Data{}
Convey("Should unmarshal simple number", func() {
unmarshalingTest(`val: 1`, d)
So(d.Val.Value(), ShouldEqual, 1)
So(d.Val.Raw, ShouldEqual, "1")
})
Convey("Should unmarshal env var", func() {
unmarshalingTest(`val: $INT`, d)
So(d.Val.Value(), ShouldEqual, 1)
So(d.Val.Raw, ShouldEqual, "$INT")
})
Convey("Should ignore empty value", func() {
unmarshalingTest(`val: `, d)
So(d.Val.Value(), ShouldEqual, 0)
So(d.Val.Raw, ShouldEqual, "")
})
})
Convey("StringValue", func() {
type Data struct {
Val StringValue `yaml:"val"`
}
d := &Data{}
Convey("Should unmarshal simple string", func() {
unmarshalingTest(`val: test`, d)
So(d.Val.Value(), ShouldEqual, "test")
So(d.Val.Raw, ShouldEqual, "test")
})
Convey("Should unmarshal env var", func() {
unmarshalingTest(`val: $STRING`, d)
So(d.Val.Value(), ShouldEqual, "test")
So(d.Val.Raw, ShouldEqual, "$STRING")
})
Convey("Should ignore empty value", func() {
unmarshalingTest(`val: `, d)
So(d.Val.Value(), ShouldEqual, "")
So(d.Val.Raw, ShouldEqual, "")
})
})
Convey("BoolValue", func() {
type Data struct {
Val BoolValue `yaml:"val"`
}
d := &Data{}
Convey("Should unmarshal bool value", func() {
unmarshalingTest(`val: true`, d)
So(d.Val.Value(), ShouldBeTrue)
So(d.Val.Raw, ShouldEqual, "true")
})
Convey("Should unmarshal explicit string", func() {
unmarshalingTest(`val: "true"`, d)
So(d.Val.Value(), ShouldBeTrue)
So(d.Val.Raw, ShouldEqual, "true")
})
Convey("Should unmarshal env var", func() {
unmarshalingTest(`val: $BOOL`, d)
So(d.Val.Value(), ShouldBeTrue)
So(d.Val.Raw, ShouldEqual, "$BOOL")
})
Convey("Should ignore empty value", func() {
unmarshalingTest(`val: `, d)
So(d.Val.Value(), ShouldBeFalse)
So(d.Val.Raw, ShouldEqual, "")
})
})
Convey("JSONValue", func() {
type Data struct {
Val JSONValue `yaml:"val"`
}
d := &Data{}
Convey("Should unmarshal variable nesting", func() {
doc := `
val:
one: 1
two: $STRING
three:
- 1
- two
- three:
inside: $STRING
four:
nested:
onemore: $INT
multiline: >
Some text with $STRING
anchor: &label $INT
anchored: *label
`
unmarshalingTest(doc, d)
type anyMap = map[interface{}]interface{}
So(d.Val.Value(), ShouldResemble, map[string]interface{}{
"one": 1,
"two": "test",
"three": []interface{}{
1, "two", anyMap{
"three": anyMap{
"inside": "test",
},
},
},
"four": anyMap{
"nested": anyMap{
"onemore": "1",
},
},
"multiline": "Some text with test\n",
"anchor": "1",
"anchored": "1",
})
So(d.Val.Raw, ShouldResemble, map[string]interface{}{
"one": 1,
"two": "$STRING",
"three": []interface{}{
1, "two", anyMap{
"three": anyMap{
"inside": "$STRING",
},
},
},
"four": anyMap{
"nested": anyMap{
"onemore": "$INT",
},
},
"multiline": "Some text with $STRING\n",
"anchor": "$INT",
"anchored": "$INT",
})
})
})
Convey("StringMapValue", func() {
type Data struct {
Val StringMapValue `yaml:"val"`
}
d := &Data{}
Convey("Should unmarshal mapping", func() {
doc := `
val:
one: 1
two: "test string"
three: $STRING
four: true
`
unmarshalingTest(doc, d)
So(d.Val.Value(), ShouldResemble, map[string]string{
"one": "1",
"two": "test string",
"three": "test",
"four": "true",
})
So(d.Val.Raw, ShouldResemble, map[string]string{
"one": "1",
"two": "test string",
"three": "$STRING",
"four": "true",
})
})
})
Reset(func() {
os.Unsetenv("INT")
os.Unsetenv("STRING")
os.Unsetenv("BOOL")
})
})
}
func unmarshalingTest(document string, out interface{}) {
err := yaml.Unmarshal([]byte(document), out)
So(err, ShouldBeNil)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment