Influx: Support PDC for Influx SQL (#103032)
* Support PDC for Influx SQL * Correctly set the gRPC transport creds dial option * Logging and refactor * Allow URLs with no scheme * Use passthrough resolver for socks proxy * Update comment * Extract url parsing and add test * Check err
This commit is contained in:
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/apache/arrow-go/v18/arrow/flight"
|
"github.com/apache/arrow-go/v18/arrow/flight"
|
||||||
"github.com/apache/arrow-go/v18/arrow/flight/flightsql"
|
"github.com/apache/arrow-go/v18/arrow/flight/flightsql"
|
||||||
"github.com/apache/arrow-go/v18/arrow/ipc"
|
"github.com/apache/arrow-go/v18/arrow/ipc"
|
||||||
"github.com/apache/arrow-go/v18/arrow/memory"
|
"github.com/apache/arrow-go/v18/arrow/memory"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
@@ -26,11 +28,17 @@ func (c *client) FlightClient() flight.Client {
|
|||||||
return c.Client.Client
|
return c.Client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFlightSQLClient(addr string, metadata metadata.MD, secure bool) (*client, error) {
|
func newFlightSQLClient(addr string, metadata metadata.MD, secure bool, proxyClient proxy.Client) (*client, error) {
|
||||||
dialOptions, err := grpcDialOptions(secure)
|
dialOptions, err := grpcDialOptions(secure, proxyClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("grpc dial options: %s", err)
|
return nil, fmt.Errorf("grpc dial options: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the secure socks proxy is enabled, we add the passthrough scheme. Otherwise, we use the raw address.
|
||||||
|
// This ensures the address is passed directly to the transport rather than trying to resolve via DNS.
|
||||||
|
if proxyClient.SecureSocksProxyEnabled() {
|
||||||
|
addr = fmt.Sprintf("passthrough:///%s", addr)
|
||||||
|
}
|
||||||
fsqlClient, err := flightsql.NewClient(addr, nil, nil, dialOptions...)
|
fsqlClient, err := flightsql.NewClient(addr, nil, nil, dialOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -38,21 +46,42 @@ func newFlightSQLClient(addr string, metadata metadata.MD, secure bool) (*client
|
|||||||
return &client{Client: fsqlClient, md: metadata}, nil
|
return &client{Client: fsqlClient, md: metadata}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func grpcDialOptions(secure bool) ([]grpc.DialOption, error) {
|
func grpcDialOptions(secure bool, proxyClient proxy.Client) ([]grpc.DialOption, error) {
|
||||||
transport := grpc.WithTransportCredentials(insecure.NewCredentials())
|
dialOptions := []grpc.DialOption{}
|
||||||
|
secureDialOpt := grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||||
|
|
||||||
if secure {
|
if secure {
|
||||||
pool, err := x509.SystemCertPool()
|
pool, err := x509.SystemCertPool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("x509: %s", err)
|
return nil, fmt.Errorf("x509: %s", err)
|
||||||
}
|
}
|
||||||
transport = grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(pool, ""))
|
secureDialOpt = grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(pool, ""))
|
||||||
|
}
|
||||||
|
dialOptions = append(dialOptions, secureDialOpt)
|
||||||
|
|
||||||
|
if proxyClient.SecureSocksProxyEnabled() {
|
||||||
|
dialer, err := proxyClient.NewSecureSocksProxyContextDialer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create influx proxy dialer: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialOptions = append(dialOptions, grpc.WithContextDialer(func(ctx context.Context, host string) (net.Conn, error) {
|
||||||
|
logger := glog.FromContext(ctx)
|
||||||
|
logger.Debug("Dialing secure socks proxy", "host", host)
|
||||||
|
conn, err := dialer.Dial("tcp", host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("not possible to dial secure socks proxy: %w", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return conn, fmt.Errorf("context canceled: %w", err)
|
||||||
|
default:
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []grpc.DialOption{
|
return dialOptions, nil
|
||||||
transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoGetWithHeaderExtraction performs a normal DoGet, but wraps the stream in a
|
// DoGetWithHeaderExtraction performs a normal DoGet, but wraps the stream in a
|
||||||
|
|||||||
@@ -97,20 +97,39 @@ type runner struct {
|
|||||||
client *client
|
client *client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseURL(endpoint string) (string, error) {
|
||||||
|
if endpoint == "" {
|
||||||
|
return "", fmt.Errorf("missing URL from datasource configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("bad URL : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := u.Host
|
||||||
|
if u.Port() == "" {
|
||||||
|
addr += ":443"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has specified an address with no scheme it can still be valid
|
||||||
|
// So we use the raw URL value
|
||||||
|
if u.Host == "" {
|
||||||
|
addr = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// runnerFromDataSource creates a runner from the datasource model (the datasource instance's configuration).
|
// runnerFromDataSource creates a runner from the datasource model (the datasource instance's configuration).
|
||||||
func runnerFromDataSource(dsInfo *models.DatasourceInfo) (*runner, error) {
|
func runnerFromDataSource(dsInfo *models.DatasourceInfo) (*runner, error) {
|
||||||
if dsInfo.URL == "" {
|
if dsInfo.URL == "" {
|
||||||
return nil, fmt.Errorf("missing URL from datasource configuration")
|
return nil, fmt.Errorf("missing URL from datasource configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(dsInfo.URL)
|
u, err := ParseURL(dsInfo.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bad URL : %s", err)
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
addr := u.Host
|
|
||||||
if u.Port() == "" {
|
|
||||||
addr += ":443"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
md := metadata.MD{}
|
md := metadata.MD{}
|
||||||
@@ -121,7 +140,7 @@ func runnerFromDataSource(dsInfo *models.DatasourceInfo) (*runner, error) {
|
|||||||
md.Set("Authorization", fmt.Sprintf("Bearer %s", dsInfo.Token))
|
md.Set("Authorization", fmt.Sprintf("Bearer %s", dsInfo.Token))
|
||||||
}
|
}
|
||||||
|
|
||||||
fsqlClient, err := newFlightSQLClient(addr, md, !dsInfo.InsecureGrpc)
|
fsqlClient, err := newFlightSQLClient(u, md, !dsInfo.InsecureGrpc, dsInfo.ProxyClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/apache/arrow-go/v18/arrow/flight/flightsql/example"
|
"github.com/apache/arrow-go/v18/arrow/flight/flightsql/example"
|
||||||
"github.com/apache/arrow-go/v18/arrow/memory"
|
"github.com/apache/arrow-go/v18/arrow/memory"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ func (suite *FSQLTestSuite) TestIntegration_QueryData() {
|
|||||||
Version: "test",
|
Version: "test",
|
||||||
HTTPMode: "proxy",
|
HTTPMode: "proxy",
|
||||||
InsecureGrpc: true,
|
InsecureGrpc: true,
|
||||||
|
ProxyClient: proxy.New(nil),
|
||||||
},
|
},
|
||||||
backend.QueryDataRequest{
|
backend.QueryDataRequest{
|
||||||
Queries: []backend.DataQuery{
|
Queries: []backend.DataQuery{
|
||||||
@@ -139,6 +141,7 @@ func TestInvalidSchema(t *testing.T) {
|
|||||||
Version: "test",
|
Version: "test",
|
||||||
HTTPMode: "proxy",
|
HTTPMode: "proxy",
|
||||||
InsecureGrpc: true,
|
InsecureGrpc: true,
|
||||||
|
ProxyClient: proxy.New(nil),
|
||||||
},
|
},
|
||||||
backend.QueryDataRequest{
|
backend.QueryDataRequest{
|
||||||
Queries: []backend.DataQuery{
|
Queries: []backend.DataQuery{
|
||||||
@@ -151,3 +154,61 @@ func TestInvalidSchema(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.Equal(t, backend.ErrorSourceDownstream, resp.Responses["A"].ErrorSource)
|
require.Equal(t, backend.ErrorSourceDownstream, resp.Responses["A"].ErrorSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
hasError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty URL",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
hasError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL without scheme",
|
||||||
|
input: "example.com",
|
||||||
|
expected: "example.com",
|
||||||
|
hasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL without scheme and with port",
|
||||||
|
input: "example.com:8181",
|
||||||
|
expected: "example.com:8181",
|
||||||
|
hasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL without port",
|
||||||
|
input: "http://example.com",
|
||||||
|
expected: "example.com:443",
|
||||||
|
hasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with http scheme",
|
||||||
|
input: "http://example.com:8080",
|
||||||
|
expected: "example.com:8080",
|
||||||
|
hasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with https scheme",
|
||||||
|
input: "https://example.com:8443",
|
||||||
|
expected: "example.com:8443",
|
||||||
|
hasError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := ParseURL(tt.input)
|
||||||
|
if tt.hasError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
|||||||
database = settings.Database
|
database = settings.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyClient, err := settings.ProxyClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("influx proxy creation failed", "error", err)
|
||||||
|
return nil, fmt.Errorf("influx proxy creation failed")
|
||||||
|
}
|
||||||
|
|
||||||
model := &models.DatasourceInfo{
|
model := &models.DatasourceInfo{
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
URL: settings.URL,
|
URL: settings.URL,
|
||||||
@@ -85,6 +91,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
|||||||
InsecureGrpc: jsonData.InsecureGrpc,
|
InsecureGrpc: jsonData.InsecureGrpc,
|
||||||
Token: settings.DecryptedSecureJSONData["token"],
|
Token: settings.DecryptedSecureJSONData["token"],
|
||||||
Timeout: opts.Timeouts.Timeout,
|
Timeout: opts.Timeouts.Timeout,
|
||||||
|
ProxyClient: proxyClient,
|
||||||
}
|
}
|
||||||
return model, nil
|
return model, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package models
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatasourceInfo struct {
|
type DatasourceInfo struct {
|
||||||
@@ -22,4 +24,6 @@ type DatasourceInfo struct {
|
|||||||
|
|
||||||
// FlightSQL grpc connection
|
// FlightSQL grpc connection
|
||||||
InsecureGrpc bool `json:"insecureGrpc"`
|
InsecureGrpc bool `json:"insecureGrpc"`
|
||||||
|
|
||||||
|
ProxyClient proxy.Client
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user