MSSQL: decouple plugin (#89597)

* decouple from core

* yarn decouple

* make health check work and azure config

* f

* driver error not needed

* merge
This commit is contained in:
Andrew Hackmann 2024-08-26 15:09:21 -05:00 committed by GitHub
parent 4755eb5176
commit eac194815e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 172 additions and 37 deletions

View File

@ -1084,7 +1084,7 @@
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
"grafanaDependency": "\u003e=10.4.0",
"grafanaVersion": "*",
"plugins": []
},

View File

@ -48,7 +48,7 @@ const (
func ProvideService(cfg *setting.Cfg) *Service {
logger := backend.NewLoggerWith("logger", "tsdb.mssql")
return &Service{
im: datasource.NewInstanceManager(newInstanceSettings(cfg, logger)),
im: datasource.NewInstanceManager(NewInstanceSettings(cfg, logger)),
logger: logger,
}
}
@ -136,12 +136,17 @@ func newMSSQL(ctx context.Context, driverName string, userFacingDefaultError str
return db, handler, nil
}
func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc {
func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc {
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
grafCfg := backend.GrafanaConfigFromContext(ctx)
sqlCfg, err := grafCfg.SQL()
if err != nil {
return nil, err
}
jsonData := sqleng.JsonData{
MaxOpenConns: cfg.SqlDatasourceMaxOpenConnsDefault,
MaxIdleConns: cfg.SqlDatasourceMaxIdleConnsDefault,
ConnMaxLifetime: cfg.SqlDatasourceMaxConnLifetimeDefault,
MaxOpenConns: sqlCfg.DefaultMaxOpenConns,
MaxIdleConns: sqlCfg.DefaultMaxIdleConns,
ConnMaxLifetime: sqlCfg.DefaultMaxConnLifetimeSeconds,
Encrypt: "false",
ConnectionTimeout: 0,
SecureDSProxy: false,
@ -176,20 +181,22 @@ func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc
UID: settings.UID,
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
}
cnnstr, err := generateConnectionString(dsInfo, cfg, azureCredentials, kerberosAuth, logger)
cnnstr, err := generateConnectionString(dsInfo, cfg.Azure.ManagedIdentityClientId, cfg.Azure.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, logger)
if err != nil {
return nil, err
}
if cfg.Env == setting.Dev {
logger.Debug("GetEngine", "connection", cnnstr)
}
driverName := "mssql"
if jsonData.AuthenticationType == azureAuthentication {
driverName = "azuresql"
}
_, handler, err := newMSSQL(ctx, driverName, cfg.UserFacingDefaultError, cfg.DataProxyRowLimit, dsInfo, cnnstr, logger, settings)
userFacingDefaultError, err := grafCfg.UserFacingDefaultError()
if err != nil {
return nil, err
}
_, handler, err := newMSSQL(ctx, driverName, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings)
if err != nil {
logger.Error("Failed connecting to MSSQL", "err", err)
@ -230,7 +237,7 @@ func ParseURL(u string, logger DebugOnlyLogger) (*url.URL, error) {
}, nil
}
func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) {
func generateConnectionString(dsInfo sqleng.DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) {
const dfltPort = "0"
var addr util.NetworkAddress
if dsInfo.URL != "" {
@ -268,7 +275,7 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, az
switch dsInfo.JsonData.AuthenticationType {
case azureAuthentication:
azureCredentialDSNFragment, err := getAzureCredentialDSNFragment(azureCredentials, cfg)
azureCredentialDSNFragment, err := getAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled)
if err != nil {
return "", err
}
@ -305,12 +312,12 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo, cfg *setting.Cfg, az
return connStr, nil
}
func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, cfg *setting.Cfg) (string, error) {
func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) {
connStr := ""
switch c := azureCredentials.(type) {
case *azcredentials.AzureManagedIdentityCredentials:
if cfg.Azure.ManagedIdentityClientId != "" {
connStr += fmt.Sprintf("user id=%s;", cfg.Azure.ManagedIdentityClientId)
if azureManagedIdentityClientId != "" {
connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId)
}
connStr += fmt.Sprintf("fedauth=%s;",
"ActiveDirectoryManagedIdentity")
@ -322,7 +329,7 @@ func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia
"ActiveDirectoryApplication",
)
case *azcredentials.AzureEntraPasswordCredentials:
if cfg.Azure.AzureEntraPasswordCredentialsEnabled {
if azureEntraPasswordCredentialsEnabled {
connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;",
c.UserId,
c.Password,
@ -360,13 +367,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
return nil, err
}
err = dsHandler.Ping()
if err != nil {
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: dsHandler.TransformQueryError(s.logger, err).Error()}, nil
}
return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
return dsHandler.CheckHealth(ctx, req)
}
func (t *mssqlQueryResultTransformer) GetConverterList() []sqlutil.StringConverter {

View File

@ -1550,7 +1550,7 @@ func TestGenerateConnectionString(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
connStr, err := generateConnectionString(tc.dataSource, nil, nil, tc.kerberosCfg, logger)
connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger)
require.NoError(t, err)
assert.Equal(t, tc.expConnStr, connStr)
})

View File

@ -152,8 +152,13 @@ func (e *DataSourceHandler) Dispose() {
e.log.Debug("DB disposed")
}
func (e *DataSourceHandler) Ping() error {
return e.db.Ping()
func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
err := e.db.Ping()
if err != nil {
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil
}
return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
}
func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {

View File

@ -0,0 +1,28 @@
package main
import (
"os"
"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/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/mssql"
)
func main() {
// Start listening to requests sent from Grafana. This call is blocking so
// it won't finish until Grafana shuts down the process or the plugin choose
// to exit by itself using os.Exit. Manage automatically manages life cycle
// of datasource instances. It accepts datasource instance factory as first
// argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of SampleDatasource (per datasource
// ID). When datasource configuration changed Dispose method will be called and
// new datasource instance created using NewSampleDatasource factory.
logger := backend.NewLoggerWith("logger", "tsdb.mssql")
cfg := setting.NewCfg()
if err := datasource.Manage("mssql", mssql.NewInstanceSettings(cfg, logger), datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}

View File

@ -17,8 +17,6 @@ const mixedPlugin = async () =>
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
const prometheusPlugin = async () =>
await import(/* webpackChunkName: "prometheusPlugin" */ 'app/plugins/datasource/prometheus/module');
const mssqlPlugin = async () =>
await import(/* webpackChunkName: "mssqlPlugin" */ 'app/plugins/datasource/mssql/module');
const alertmanagerPlugin = async () =>
await import(/* webpackChunkName: "alertmanagerPlugin" */ 'app/plugins/datasource/alertmanager/module');
@ -79,7 +77,6 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
'core:plugin/influxdb': influxdbPlugin,
'core:plugin/loki': lokiPlugin,
'core:plugin/mixed': mixedPlugin,
'core:plugin/mssql': mssqlPlugin,
'core:plugin/prometheus': prometheusPlugin,
'core:plugin/alertmanager': alertmanagerPlugin,
// panels

View File

@ -0,0 +1 @@
# Changelog

View File

@ -11,7 +11,9 @@ import {
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { ConnectionLimits, useMigrateDatabaseFields } from '@grafana/sql';
import { NumberInput } from '@grafana/sql/src/components/configuration/NumberInput';
import {
Alert,
FieldSet,
@ -25,8 +27,6 @@ import {
Field,
Switch,
} from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { config } from 'app/core/config';
import { AzureAuthSettings } from '../azureauth/AzureAuthSettings';
import {
@ -92,7 +92,10 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
};
const onConnectionTimeoutChanged = (connectionTimeout?: number) => {
updateDatasourcePluginJsonDataOption(props, 'connectionTimeout', connectionTimeout ?? 0);
if (connectionTimeout && connectionTimeout < 0) {
connectionTimeout = 0;
}
updateDatasourcePluginJsonDataOption(props, 'connectionTimeout', connectionTimeout);
};
const buildAuthenticationOptions = (): Array<SelectableValue<MSSQLAuthenticationType>> => {
@ -366,9 +369,8 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
>
<NumberInput
width={LONG_WIDTH}
placeholder="60"
min={0}
value={jsonData.connectionTimeout}
defaultValue={60}
value={jsonData.connectionTimeout || 0}
onChange={onConnectionTimeoutChanged}
/>
</Field>

View File

@ -0,0 +1,41 @@
{
"name": "@grafana-plugins/mssql",
"description": "MSSQL data source plugin",
"private": true,
"version": "11.3.0-pre",
"dependencies": {
"@emotion/css": "11.11.2",
"@grafana/data": "workspace:*",
"@grafana/experimental": "1.7.12",
"@grafana/runtime": "11.3.0-pre",
"@grafana/sql": "11.3.0-pre",
"@grafana/ui": "11.3.0-pre",
"lodash": "4.17.21",
"react": "18.2.0",
"rxjs": "7.8.1",
"tslib": "2.6.3"
},
"devDependencies": {
"@grafana/e2e-selectors": "workspace:*",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.4",
"@types/node": "20.14.2",
"@types/react": "18.3.3",
"@types/testing-library__jest-dom": "5.14.9",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"webpack": "5.91.0"
},
"peerDependencies": {
"@grafana/runtime": "*"
},
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
"build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
"dev": "webpack -w -c ./webpack.config.ts --env development"
},
"packageManager": "yarn@4.4.0"
}

View File

@ -2,6 +2,7 @@
"type": "datasource",
"name": "Microsoft SQL Server",
"id": "mssql",
"executable": "gpx_mssql",
"category": "sql",
"info": {
@ -13,7 +14,11 @@
"logos": {
"small": "img/sql_server_logo.svg",
"large": "img/sql_server_logo.svg"
}
},
"version": "%VERSION%"
},
"dependencies": {
"grafanaDependency": ">=10.4.0"
},
"alerting": true,

View File

@ -0,0 +1,4 @@
{
"extends": "@grafana/plugin-configs/tsconfig.json",
"include": ["."]
}

View File

@ -0,0 +1,4 @@
import config from '@grafana/plugin-configs/webpack.config';
// eslint-disable-next-line no-barrel-files/no-barrel-files
export default config;

View File

@ -3102,6 +3102,37 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql":
version: 0.0.0-use.local
resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql"
dependencies:
"@emotion/css": "npm:11.11.2"
"@grafana/data": "workspace:*"
"@grafana/e2e-selectors": "workspace:*"
"@grafana/experimental": "npm:1.7.12"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.0-pre"
"@grafana/sql": "npm:11.3.0-pre"
"@grafana/ui": "npm:11.3.0-pre"
"@testing-library/react": "npm:15.0.2"
"@testing-library/user-event": "npm:14.5.2"
"@types/jest": "npm:29.5.12"
"@types/lodash": "npm:4.17.4"
"@types/node": "npm:20.14.2"
"@types/react": "npm:18.3.3"
"@types/testing-library__jest-dom": "npm:5.14.9"
lodash: "npm:4.17.21"
react: "npm:18.2.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.6.3"
typescript: "npm:5.4.5"
webpack: "npm:5.91.0"
peerDependencies:
"@grafana/runtime": "*"
languageName: unknown
linkType: soft
"@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql":
version: 0.0.0-use.local
resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql"
@ -9821,6 +9852,13 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash@npm:4.17.4":
version: 4.17.4
resolution: "@types/lodash@npm:4.17.4"
checksum: 10/3ec19f9fc48200006e71733e08bcb1478b0398673657fcfb21a8643d41a80bcce09a01000077c3b23a3c6d86b9b314abe0672a8fdfc0fd66b893bd41955cfab8
languageName: node
linkType: hard
"@types/logfmt@npm:^1.2.3":
version: 1.2.6
resolution: "@types/logfmt@npm:1.2.6"
@ -9932,6 +9970,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:20.14.2":
version: 20.14.2
resolution: "@types/node@npm:20.14.2"
dependencies:
undici-types: "npm:~5.26.4"
checksum: 10/c38e47b190fa0a8bdfde24b036dddcf9401551f2fb170a90ff33625c7d6f218907e81c74e0fa6e394804a32623c24c60c50e249badc951007830f0d02c48ee0f
languageName: node
linkType: hard
"@types/node@npm:^18.0.0":
version: 18.19.33
resolution: "@types/node@npm:18.19.33"