mirror of
https://github.com/grafana/grafana.git
synced 2026-08-11 13:44:59 -05:00
SQL: Migrate to use SDK contracts (#36635)
* convert SQLs to use sdk contracts * make draft * postgres * intermedia * get datasourceinfo filled at the beginning of the service * move the interval into package because of cyclict import and fix all postgres tests * fix mysql test * fix mssql * fix the test for pr https://github.com/grafana/grafana/issues/35839 * fix some issue about intervalv2 package * update sql test * wire migration for SQLs * add sqls to the background process * make it register instead of register and start * revert formatting * fix tests * fix linter * remove integration test * Postgres test fix Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
co-authored by
Marcus Efraimsson
parent
51aeb717a4
commit
c989f0fbbe
+10
-11
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/gtime"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
)
|
||||
|
||||
@@ -26,8 +26,7 @@ func newPostgresMacroEngine(timescaledb bool) sqleng.SQLMacroEngine {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *postgresMacroEngine) Interpolate(query plugins.DataSubQuery, timeRange plugins.DataTimeRange,
|
||||
sql string) (string, error) {
|
||||
func (m *postgresMacroEngine) Interpolate(query *backend.DataQuery, timeRange backend.TimeRange, sql string) (string, error) {
|
||||
// TODO: Handle error
|
||||
rExp, _ := regexp.Compile(sExpr)
|
||||
var macroError error
|
||||
@@ -67,7 +66,7 @@ func (m *postgresMacroEngine) Interpolate(query plugins.DataSubQuery, timeRange
|
||||
}
|
||||
|
||||
//nolint: gocyclo
|
||||
func (m *postgresMacroEngine) evaluateMacro(timeRange plugins.DataTimeRange, query plugins.DataSubQuery, name string, args []string) (string, error) {
|
||||
func (m *postgresMacroEngine) evaluateMacro(timeRange backend.TimeRange, query *backend.DataQuery, name string, args []string) (string, error) {
|
||||
switch name {
|
||||
case "__time":
|
||||
if len(args) == 0 {
|
||||
@@ -84,11 +83,11 @@ func (m *postgresMacroEngine) evaluateMacro(timeRange plugins.DataTimeRange, que
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
|
||||
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], timeRange.From.UTC().Format(time.RFC3339Nano), timeRange.To.UTC().Format(time.RFC3339Nano)), nil
|
||||
case "__timeFrom":
|
||||
return fmt.Sprintf("'%s'", timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil
|
||||
return fmt.Sprintf("'%s'", timeRange.From.UTC().Format(time.RFC3339Nano)), nil
|
||||
case "__timeTo":
|
||||
return fmt.Sprintf("'%s'", timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
|
||||
return fmt.Sprintf("'%s'", timeRange.To.UTC().Format(time.RFC3339Nano)), nil
|
||||
case "__timeGroup":
|
||||
if len(args) < 2 {
|
||||
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
|
||||
@@ -123,16 +122,16 @@ func (m *postgresMacroEngine) evaluateMacro(timeRange plugins.DataTimeRange, que
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], timeRange.GetFromAsSecondsEpoch(), args[0], timeRange.GetToAsSecondsEpoch()), nil
|
||||
return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], timeRange.From.UTC().Unix(), args[0], timeRange.To.UTC().Unix()), nil
|
||||
case "__unixEpochNanoFilter":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], timeRange.GetFromAsTimeUTC().UnixNano(), args[0], timeRange.GetToAsTimeUTC().UnixNano()), nil
|
||||
return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], timeRange.From.UTC().UnixNano(), args[0], timeRange.To.UTC().UnixNano()), nil
|
||||
case "__unixEpochNanoFrom":
|
||||
return fmt.Sprintf("%d", timeRange.GetFromAsTimeUTC().UnixNano()), nil
|
||||
return fmt.Sprintf("%d", timeRange.From.UTC().UnixNano()), nil
|
||||
case "__unixEpochNanoTo":
|
||||
return fmt.Sprintf("%d", timeRange.GetToAsTimeUTC().UnixNano()), nil
|
||||
return fmt.Sprintf("%d", timeRange.To.UTC().UnixNano()), nil
|
||||
case "__unixEpochGroup":
|
||||
if len(args) < 2 {
|
||||
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
|
||||
|
||||
@@ -2,13 +2,11 @@ package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -17,12 +15,12 @@ func TestMacroEngine(t *testing.T) {
|
||||
engine := newPostgresMacroEngine(timescaledbEnabled)
|
||||
timescaledbEnabled = true
|
||||
engineTS := newPostgresMacroEngine(timescaledbEnabled)
|
||||
query := plugins.DataSubQuery{}
|
||||
query := &backend.DataQuery{}
|
||||
|
||||
t.Run("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func(t *testing.T) {
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: to}
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
|
||||
t.Run("interpolate __time function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
|
||||
@@ -151,9 +149,10 @@ func TestMacroEngine(t *testing.T) {
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := plugins.NewDataTimeRange(
|
||||
strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10),
|
||||
strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
@@ -177,9 +176,10 @@ func TestMacroEngine(t *testing.T) {
|
||||
t.Run("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
|
||||
timeRange := plugins.NewDataTimeRange(
|
||||
strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10),
|
||||
strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
t.Run("interpolate __timeFilter function", func(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
@@ -203,9 +203,10 @@ func TestMacroEngine(t *testing.T) {
|
||||
t.Run("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func(t *testing.T) {
|
||||
from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC)
|
||||
to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC)
|
||||
timeRange := plugins.NewDataTimeRange(
|
||||
strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
|
||||
|
||||
timeRange := backend.TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
require.Equal(t, "1960-02-01T07:00:00.5Z", from.Format(time.RFC3339Nano))
|
||||
require.Equal(t, "1980-02-03T08:00:00.5Z", to.Format(time.RFC3339Nano))
|
||||
|
||||
@@ -219,27 +220,27 @@ func TestMacroEngine(t *testing.T) {
|
||||
|
||||
func TestMacroEngineConcurrency(t *testing.T) {
|
||||
engine := newPostgresMacroEngine(false)
|
||||
query1 := plugins.DataSubQuery{
|
||||
Model: simplejson.New(),
|
||||
query1 := backend.DataQuery{
|
||||
JSON: []byte{},
|
||||
}
|
||||
query2 := plugins.DataSubQuery{
|
||||
Model: simplejson.New(),
|
||||
query2 := backend.DataQuery{
|
||||
JSON: []byte{},
|
||||
}
|
||||
from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
|
||||
to := from.Add(5 * time.Minute)
|
||||
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: to}
|
||||
timeRange := backend.TimeRange{From: from, To: to}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func(query plugins.DataSubQuery) {
|
||||
go func(query backend.DataQuery) {
|
||||
defer wg.Done()
|
||||
_, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
|
||||
_, err := engine.Interpolate(&query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
|
||||
require.NoError(t, err)
|
||||
}(query1)
|
||||
|
||||
go func(query plugins.DataSubQuery) {
|
||||
_, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
|
||||
go func(query backend.DataQuery) {
|
||||
_, err := engine.Interpolate(&query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
|
||||
require.NoError(t, err)
|
||||
defer wg.Done()
|
||||
}(query2)
|
||||
|
||||
@@ -1,72 +1,124 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) *PostgresService {
|
||||
logger := log.New("tsdb.postgres")
|
||||
return &PostgresService{
|
||||
var logger = log.New("tsdb.postgres")
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, manager backendplugin.Manager) (*Service, error) {
|
||||
s := &Service{
|
||||
Cfg: cfg,
|
||||
logger: logger,
|
||||
tlsManager: newTLSManager(logger, cfg.DataPath),
|
||||
}
|
||||
s.im = datasource.NewInstanceManager(s.newInstanceSettings())
|
||||
factory := coreplugin.New(backend.ServeOpts{
|
||||
QueryDataHandler: s,
|
||||
})
|
||||
|
||||
if err := manager.Register("postgres", factory); err != nil {
|
||||
logger.Error("Failed to register plugin", "error", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type PostgresService struct {
|
||||
type Service struct {
|
||||
Cfg *setting.Cfg
|
||||
logger log.Logger
|
||||
tlsManager tlsSettingsProvider
|
||||
im instancemgmt.InstanceManager
|
||||
}
|
||||
|
||||
//nolint: staticcheck // plugins.DataPlugin deprecated
|
||||
func (s *PostgresService) NewExecutor(datasource *models.DataSource) (plugins.DataPlugin, error) {
|
||||
s.logger.Debug("Creating Postgres query endpoint")
|
||||
|
||||
cnnstr, err := s.generateConnectionString(datasource)
|
||||
func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*sqleng.DataSourceHandler, error) {
|
||||
i, err := s.im.Get(pluginCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance := i.(*sqleng.DataSourceHandler)
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
if s.Cfg.Env == setting.Dev {
|
||||
s.logger.Debug("getEngine", "connection", cnnstr)
|
||||
}
|
||||
|
||||
config := sqleng.DataPluginConfiguration{
|
||||
DriverName: "postgres",
|
||||
ConnectionString: cnnstr,
|
||||
Datasource: datasource,
|
||||
MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"},
|
||||
}
|
||||
|
||||
queryResultTransformer := postgresQueryResultTransformer{
|
||||
log: s.logger,
|
||||
}
|
||||
|
||||
timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false)
|
||||
|
||||
plugin, err := sqleng.NewDataPlugin(config, &queryResultTransformer, newPostgresMacroEngine(timescaledb),
|
||||
s.logger)
|
||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
dsInfo, err := s.getDSInfo(req.PluginContext)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed connecting to Postgres", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
return dsInfo.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
s.logger.Debug("Successfully connected to Postgres")
|
||||
return plugin, nil
|
||||
func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc {
|
||||
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
logger.Debug("Creating Postgres query endpoint")
|
||||
jsonData := sqleng.JsonData{
|
||||
MaxOpenConns: 0,
|
||||
MaxIdleConns: 2,
|
||||
ConnMaxLifetime: 14400,
|
||||
Timescaledb: false,
|
||||
ConfigurationMethod: "file-path",
|
||||
}
|
||||
|
||||
err := json.Unmarshal(settings.JSONData, &jsonData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading settings: %w", err)
|
||||
}
|
||||
dsInfo := sqleng.DataSourceInfo{
|
||||
JsonData: jsonData,
|
||||
URL: settings.URL,
|
||||
User: settings.User,
|
||||
Database: settings.Database,
|
||||
ID: settings.ID,
|
||||
Updated: settings.Updated,
|
||||
UID: settings.UID,
|
||||
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
}
|
||||
|
||||
cnnstr, err := s.generateConnectionString(dsInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.Cfg.Env == setting.Dev {
|
||||
logger.Debug("getEngine", "connection", cnnstr)
|
||||
}
|
||||
|
||||
config := sqleng.DataPluginConfiguration{
|
||||
DriverName: "postgres",
|
||||
ConnectionString: cnnstr,
|
||||
DSInfo: dsInfo,
|
||||
MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"},
|
||||
}
|
||||
|
||||
queryResultTransformer := postgresQueryResultTransformer{
|
||||
log: logger,
|
||||
}
|
||||
|
||||
handler, err := sqleng.NewQueryDataHandler(config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb),
|
||||
logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed connecting to Postgres", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("Successfully connected to Postgres")
|
||||
return handler, nil
|
||||
}
|
||||
}
|
||||
|
||||
// escape single quotes and backslashes in Postgres connection string parameters.
|
||||
@@ -74,14 +126,14 @@ func escape(input string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`)
|
||||
}
|
||||
|
||||
func (s *PostgresService) generateConnectionString(datasource *models.DataSource) (string, error) {
|
||||
func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) {
|
||||
var host string
|
||||
var port int
|
||||
if strings.HasPrefix(datasource.Url, "/") {
|
||||
host = datasource.Url
|
||||
s.logger.Debug("Generating connection string with Unix socket specifier", "socket", host)
|
||||
if strings.HasPrefix(dsInfo.URL, "/") {
|
||||
host = dsInfo.URL
|
||||
logger.Debug("Generating connection string with Unix socket specifier", "socket", host)
|
||||
} else {
|
||||
sp := strings.SplitN(datasource.Url, ":", 2)
|
||||
sp := strings.SplitN(dsInfo.URL, ":", 2)
|
||||
host = sp[0]
|
||||
if len(sp) > 1 {
|
||||
var err error
|
||||
@@ -90,19 +142,19 @@ func (s *PostgresService) generateConnectionString(datasource *models.DataSource
|
||||
return "", errutil.Wrapf(err, "invalid port in host specifier %q", sp[1])
|
||||
}
|
||||
|
||||
s.logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port)
|
||||
logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port)
|
||||
} else {
|
||||
s.logger.Debug("Generating connection string with network host", "host", host)
|
||||
logger.Debug("Generating connection string with network host", "host", host)
|
||||
}
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'",
|
||||
escape(datasource.User), escape(datasource.DecryptedPassword()), escape(host), escape(datasource.Database))
|
||||
escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database))
|
||||
if port > 0 {
|
||||
connStr += fmt.Sprintf(" port=%d", port)
|
||||
}
|
||||
|
||||
tlsSettings, err := s.tlsManager.getTLSSettings(datasource)
|
||||
tlsSettings, err := s.tlsManager.getTLSSettings(dsInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -111,19 +163,19 @@ func (s *PostgresService) generateConnectionString(datasource *models.DataSource
|
||||
|
||||
// Attach root certificate if provided
|
||||
if tlsSettings.RootCertFile != "" {
|
||||
s.logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile)
|
||||
logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile)
|
||||
connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile))
|
||||
}
|
||||
|
||||
// Attach client certificate and key if both are provided
|
||||
if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" {
|
||||
s.logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile)
|
||||
logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile)
|
||||
connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile))
|
||||
} else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" {
|
||||
return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified")
|
||||
}
|
||||
|
||||
s.logger.Debug("Generated Postgres connection string successfully")
|
||||
logger.Debug("Generated Postgres connection string successfully")
|
||||
return connStr, nil
|
||||
}
|
||||
|
||||
|
||||
+298
-317
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,26 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
)
|
||||
|
||||
var validateCertFunc = validateCertFilePaths
|
||||
var writeCertFileFunc = writeCertFile
|
||||
|
||||
type certFileType int
|
||||
|
||||
const (
|
||||
rootCert = iota
|
||||
clientCert
|
||||
clientKey
|
||||
)
|
||||
|
||||
type tlsSettingsProvider interface {
|
||||
getTLSSettings(datasource *models.DataSource) (tlsSettings, error)
|
||||
getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error)
|
||||
}
|
||||
|
||||
type datasourceCacheManager struct {
|
||||
@@ -48,46 +57,37 @@ type tlsSettings struct {
|
||||
CertKeyFile string
|
||||
}
|
||||
|
||||
func (m *tlsManager) getTLSSettings(datasource *models.DataSource) (tlsSettings, error) {
|
||||
tlsMode := strings.TrimSpace(strings.ToLower(datasource.JsonData.Get("sslmode").MustString("verify-full")))
|
||||
isTLSDisabled := tlsMode == "disable"
|
||||
func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) {
|
||||
tlsconfig := tlsSettings{
|
||||
Mode: dsInfo.JsonData.Mode,
|
||||
}
|
||||
|
||||
settings := tlsSettings{}
|
||||
settings.Mode = tlsMode
|
||||
isTLSDisabled := (tlsconfig.Mode == "disable")
|
||||
|
||||
if isTLSDisabled {
|
||||
m.logger.Debug("Postgres TLS/SSL is disabled")
|
||||
return settings, nil
|
||||
return tlsconfig, nil
|
||||
}
|
||||
|
||||
m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsMode)
|
||||
m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsconfig.Mode)
|
||||
|
||||
settings.ConfigurationMethod = strings.TrimSpace(
|
||||
strings.ToLower(datasource.JsonData.Get("tlsConfigurationMethod").MustString("file-path")))
|
||||
tlsconfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod
|
||||
tlsconfig.RootCertFile = dsInfo.JsonData.RootCertFile
|
||||
tlsconfig.CertFile = dsInfo.JsonData.CertFile
|
||||
tlsconfig.CertKeyFile = dsInfo.JsonData.CertKeyFile
|
||||
|
||||
if settings.ConfigurationMethod == "file-content" {
|
||||
if err := m.writeCertFiles(datasource, &settings); err != nil {
|
||||
return settings, err
|
||||
if tlsconfig.ConfigurationMethod == "file-content" {
|
||||
if err := m.writeCertFiles(dsInfo, &tlsconfig); err != nil {
|
||||
return tlsconfig, err
|
||||
}
|
||||
} else {
|
||||
settings.RootCertFile = datasource.JsonData.Get("sslRootCertFile").MustString("")
|
||||
settings.CertFile = datasource.JsonData.Get("sslCertFile").MustString("")
|
||||
settings.CertKeyFile = datasource.JsonData.Get("sslKeyFile").MustString("")
|
||||
if err := validateCertFunc(settings.RootCertFile, settings.CertFile, settings.CertKeyFile); err != nil {
|
||||
return settings, err
|
||||
if err := validateCertFunc(tlsconfig.RootCertFile, tlsconfig.CertFile, tlsconfig.CertKeyFile); err != nil {
|
||||
return tlsconfig, err
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
return tlsconfig, nil
|
||||
}
|
||||
|
||||
type certFileType int
|
||||
|
||||
const (
|
||||
rootCert = iota
|
||||
clientCert
|
||||
clientKey
|
||||
)
|
||||
|
||||
func (t certFileType) String() string {
|
||||
switch t {
|
||||
case rootCert:
|
||||
@@ -118,8 +118,7 @@ func getFileName(dataDir string, fileType certFileType) string {
|
||||
}
|
||||
|
||||
// writeCertFile writes a certificate file.
|
||||
func writeCertFile(
|
||||
ds *models.DataSource, logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
func writeCertFile(logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
fileContent = strings.TrimSpace(fileContent)
|
||||
if fileContent != "" {
|
||||
logger.Debug("Writing cert file", "path", generatedFilePath)
|
||||
@@ -146,30 +145,28 @@ func writeCertFile(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *tlsManager) writeCertFiles(ds *models.DataSource, settings *tlsSettings) error {
|
||||
func (m *tlsManager) writeCertFiles(dsInfo sqleng.DataSourceInfo, tlsconfig *tlsSettings) error {
|
||||
m.logger.Debug("Writing TLS certificate files to disk")
|
||||
decrypted := ds.DecryptedValues()
|
||||
tlsRootCert := decrypted["tlsCACert"]
|
||||
tlsClientCert := decrypted["tlsClientCert"]
|
||||
tlsClientKey := decrypted["tlsClientKey"]
|
||||
|
||||
tlsRootCert := dsInfo.DecryptedSecureJSONData["tlsCACert"]
|
||||
tlsClientCert := dsInfo.DecryptedSecureJSONData["tlsClientCert"]
|
||||
tlsClientKey := dsInfo.DecryptedSecureJSONData["tlsClientKey"]
|
||||
if tlsRootCert == "" && tlsClientCert == "" && tlsClientKey == "" {
|
||||
m.logger.Debug("No TLS/SSL certificates provided")
|
||||
}
|
||||
|
||||
// Calculate all files path
|
||||
workDir := filepath.Join(m.dataPath, "tls", ds.Uid+"generatedTLSCerts")
|
||||
settings.RootCertFile = getFileName(workDir, rootCert)
|
||||
settings.CertFile = getFileName(workDir, clientCert)
|
||||
settings.CertKeyFile = getFileName(workDir, clientKey)
|
||||
workDir := filepath.Join(m.dataPath, "tls", dsInfo.UID+"generatedTLSCerts")
|
||||
tlsconfig.RootCertFile = getFileName(workDir, rootCert)
|
||||
tlsconfig.CertFile = getFileName(workDir, clientCert)
|
||||
tlsconfig.CertKeyFile = getFileName(workDir, clientKey)
|
||||
|
||||
// Find datasource in the cache, if found, skip writing files
|
||||
cacheKey := strconv.Itoa(int(ds.Id))
|
||||
cacheKey := strconv.Itoa(int(dsInfo.ID))
|
||||
m.dsCacheInstance.locker.RLock(cacheKey)
|
||||
item, ok := m.dsCacheInstance.cache.Load(cacheKey)
|
||||
m.dsCacheInstance.locker.RUnlock(cacheKey)
|
||||
if ok {
|
||||
if item.(int) == ds.Version {
|
||||
if !item.(time.Time).Before(dsInfo.Updated) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -179,7 +176,7 @@ func (m *tlsManager) writeCertFiles(ds *models.DataSource, settings *tlsSettings
|
||||
|
||||
item, ok = m.dsCacheInstance.cache.Load(cacheKey)
|
||||
if ok {
|
||||
if item.(int) == ds.Version {
|
||||
if !item.(time.Time).Before(dsInfo.Updated) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -195,18 +192,18 @@ func (m *tlsManager) writeCertFiles(ds *models.DataSource, settings *tlsSettings
|
||||
}
|
||||
}
|
||||
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsRootCert, settings.RootCertFile); err != nil {
|
||||
if err = writeCertFileFunc(m.logger, tlsRootCert, tlsconfig.RootCertFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsClientCert, settings.CertFile); err != nil {
|
||||
if err = writeCertFileFunc(m.logger, tlsClientCert, tlsconfig.CertFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsClientKey, settings.CertKeyFile); err != nil {
|
||||
if err = writeCertFileFunc(m.logger, tlsClientKey, tlsconfig.CertKeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update datasource cache
|
||||
m.dsCacheInstance.cache.Store(cacheKey, ds.Version)
|
||||
m.dsCacheInstance.cache.Store(cacheKey, dsInfo.Updated)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -30,16 +29,17 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
dsCacheInstance: datasourceCacheManager{locker: newLocker()},
|
||||
dataPath: cfg.DataPath,
|
||||
}
|
||||
|
||||
jsonData := simplejson.NewFromAny(map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"tlsConfigurationMethod": "file-content",
|
||||
})
|
||||
secureJSONData := securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
jsonData := sqleng.JsonData{
|
||||
Mode: "verify-full",
|
||||
ConfigurationMethod: "file-content",
|
||||
}
|
||||
secureJSONData := map[string]string{
|
||||
"tlsClientCert": "I am client certification",
|
||||
"tlsClientKey": "I am client key",
|
||||
"tlsCACert": "I am CA certification",
|
||||
})
|
||||
}
|
||||
|
||||
updateTime := time.Now().Add(-5 * time.Minute)
|
||||
|
||||
mockValidateCertFilePaths()
|
||||
t.Cleanup(resetValidateCertFilePaths)
|
||||
@@ -49,13 +49,13 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
wg.Add(10)
|
||||
for id := int64(1); id <= 10; id++ {
|
||||
go func(id int64) {
|
||||
ds := &models.DataSource{
|
||||
Id: id,
|
||||
Version: 1,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
ds := sqleng.DataSourceInfo{
|
||||
ID: id,
|
||||
Updated: updateTime,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
DecryptedSecureJSONData: secureJSONData,
|
||||
UID: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(ds, &s)
|
||||
@@ -67,9 +67,9 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
|
||||
t.Run("check cache creation is succeed", func(t *testing.T) {
|
||||
for id := int64(1); id <= 10; id++ {
|
||||
version, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id)))
|
||||
updated, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int(1), version)
|
||||
require.Equal(t, updateTime, updated)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -82,13 +82,13 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
wg1.Add(5)
|
||||
for id := int64(1); id <= 5; id++ {
|
||||
go func(id int64) {
|
||||
ds := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 2,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
ds := sqleng.DataSourceInfo{
|
||||
ID: 1,
|
||||
Updated: updateTime,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
DecryptedSecureJSONData: secureJSONData,
|
||||
UID: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(ds, &s)
|
||||
@@ -97,25 +97,25 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
}(id)
|
||||
}
|
||||
wg1.Wait()
|
||||
assert.Equal(t, writeCertFileCallNum, 3)
|
||||
assert.Equal(t, writeCertFileCallNum, 0)
|
||||
})
|
||||
|
||||
t.Run("cache is updated with the last datasource version", func(t *testing.T) {
|
||||
dsV2 := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 2,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
dsV2 := sqleng.DataSourceInfo{
|
||||
ID: 1,
|
||||
Updated: updateTime.Add(time.Minute),
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
DecryptedSecureJSONData: secureJSONData,
|
||||
UID: "testData",
|
||||
}
|
||||
dsV3 := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 3,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
dsV3 := sqleng.DataSourceInfo{
|
||||
ID: 1,
|
||||
Updated: updateTime.Add(2 * time.Minute),
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
DecryptedSecureJSONData: secureJSONData,
|
||||
UID: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(dsV2, &s)
|
||||
@@ -124,7 +124,7 @@ func TestDataSourceCacheManager(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
version, ok := mng.dsCacheInstance.cache.Load("1")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int(3), version)
|
||||
require.Equal(t, updateTime.Add(2*time.Minute), version)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -173,36 +173,39 @@ func TestGetTLSSettings(t *testing.T) {
|
||||
|
||||
mockValidateCertFilePaths()
|
||||
t.Cleanup(resetValidateCertFilePaths)
|
||||
|
||||
updatedTime := time.Now()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
expErr string
|
||||
jsonData map[string]interface{}
|
||||
jsonData sqleng.JsonData
|
||||
secureJSONData map[string]string
|
||||
uid string
|
||||
tlsSettings tlsSettings
|
||||
version int
|
||||
updated time.Time
|
||||
}{
|
||||
{
|
||||
desc: "Custom TLS authentication disabled",
|
||||
version: 1,
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "disable",
|
||||
"sslRootCertFile": "i/am/coding/ca.crt",
|
||||
"sslCertFile": "i/am/coding/client.crt",
|
||||
"sslKeyFile": "i/am/coding/client.key",
|
||||
"tlsConfigurationMethod": "file-path",
|
||||
updated: updatedTime,
|
||||
jsonData: sqleng.JsonData{
|
||||
Mode: "disable",
|
||||
RootCertFile: "i/am/coding/ca.crt",
|
||||
CertFile: "i/am/coding/client.crt",
|
||||
CertKeyFile: "i/am/coding/client.key",
|
||||
ConfigurationMethod: "file-path",
|
||||
},
|
||||
tlsSettings: tlsSettings{Mode: "disable"},
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS authentication with file path",
|
||||
version: 2,
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"sslRootCertFile": "i/am/coding/ca.crt",
|
||||
"sslCertFile": "i/am/coding/client.crt",
|
||||
"sslKeyFile": "i/am/coding/client.key",
|
||||
"tlsConfigurationMethod": "file-path",
|
||||
updated: updatedTime.Add(time.Minute),
|
||||
jsonData: sqleng.JsonData{
|
||||
Mode: "verify-full",
|
||||
ConfigurationMethod: "file-path",
|
||||
RootCertFile: "i/am/coding/ca.crt",
|
||||
CertFile: "i/am/coding/client.crt",
|
||||
CertKeyFile: "i/am/coding/client.key",
|
||||
},
|
||||
tlsSettings: tlsSettings{
|
||||
Mode: "verify-full",
|
||||
@@ -214,11 +217,11 @@ func TestGetTLSSettings(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS mode verify-full with certificate files content",
|
||||
version: 3,
|
||||
updated: updatedTime.Add(2 * time.Minute),
|
||||
uid: "xxx",
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"tlsConfigurationMethod": "file-content",
|
||||
jsonData: sqleng.JsonData{
|
||||
Mode: "verify-full",
|
||||
ConfigurationMethod: "file-content",
|
||||
},
|
||||
secureJSONData: map[string]string{
|
||||
"tlsCACert": "I am CA certification",
|
||||
@@ -244,12 +247,11 @@ func TestGetTLSSettings(t *testing.T) {
|
||||
dataPath: cfg.DataPath,
|
||||
}
|
||||
|
||||
jsonData := simplejson.NewFromAny(tt.jsonData)
|
||||
ds := &models.DataSource{
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: securejsondata.GetEncryptedJsonData(tt.secureJSONData),
|
||||
Uid: tt.uid,
|
||||
Version: tt.version,
|
||||
ds := sqleng.DataSourceInfo{
|
||||
JsonData: tt.jsonData,
|
||||
DecryptedSecureJSONData: tt.secureJSONData,
|
||||
UID: tt.uid,
|
||||
Updated: tt.updated,
|
||||
}
|
||||
|
||||
settings, err = mng.getTLSSettings(ds)
|
||||
@@ -278,7 +280,7 @@ func resetValidateCertFilePaths() {
|
||||
|
||||
func mockWriteCertFile() {
|
||||
writeCertFileCallNum = 0
|
||||
writeCertFileFunc = func(ds *models.DataSource, logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
writeCertFileFunc = func(logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
writeCertFileCallNum++
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user