mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
eb8af01a8a
commit
fcebd713a5
@ -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 />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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) {
|
|||||||
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)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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'
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
|
|
||||||
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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 (
|
|||||||
"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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
|
|
||||||
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
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *notificationsAsConfig) mapToNotificationFromConfig() *notificationsAsConfig {
|
|
||||||
r := ¬ificationsAsConfig{}
|
|
||||||
if cfg == nil {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, notification := range cfg.Notifications {
|
|
||||||
r.Notifications = append(r.Notifications, ¬ificationFromConfig{
|
|
||||||
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) (
|
|||||||
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
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
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
|
||||||
@ -39,4 +39,4 @@ delete_notifiers:
|
|||||||
org_id: 0
|
org_id: 0
|
||||||
uid: "notifier3"
|
uid: "notifier3"
|
||||||
- name: Deleted notification with whitespaces in name
|
- name: Deleted notification with whitespaces in name
|
||||||
uid: "notifier4"
|
uid: "notifier4"
|
||||||
|
@ -1,30 +1,61 @@
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
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 := ¬ificationsAsConfig{}
|
||||||
|
if cfg == nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notification := range cfg.Notifications {
|
||||||
|
r.Notifications = append(r.Notifications, ¬ificationFromConfig{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
206
pkg/services/provisioning/values/values.go
Normal file
206
pkg/services/provisioning/values/values.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// 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
|
||||||
|
}
|
210
pkg/services/provisioning/values/values_test.go
Normal file
210
pkg/services/provisioning/values/values_test.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user