From 5007bce6ad17a7d6adb12d29db90f8f5ff18f896 Mon Sep 17 00:00:00 2001 From: Olaf Villadsen Date: Mon, 1 Jun 2026 09:32:37 -0700 Subject: [PATCH] feat: add Skip TLS Verify toggle to datasource config Adds a frontend toggle and backend support for skipping TLS certificate verification, useful for development environments with self-signed certs. Co-Authored-By: Claude Sonnet 4.6 --- pkg/plugin/datasource.go | 31 +++++++++++++---- pkg/plugin/transport.go | 62 +++++++++++++++++++++++++++++++++ src/components/ConfigEditor.tsx | 9 ++++- src/types.ts | 1 + 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 pkg/plugin/transport.go diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index 410a7d9..e124029 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + neturl "net/url" "sort" "strconv" "strings" @@ -40,36 +41,52 @@ func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSetti if jsonErr != nil { return nil, jsonErr } - url := options.Url + rawURL := options.Url username := options.Username // settings contains secure inputs in .DecryptedSecureJSONData in a string:string map password := settings.DecryptedSecureJSONData["password"] - client := client.NewClient(url, username, password) - openErr := client.Open() + var insecureHost string + if options.SkipTlsVerify { + if parsedURL, err := neturl.Parse(rawURL); err == nil { + insecureHost = parsedURL.Host + registerInsecureHost(insecureHost) + } + } + + haystackClient := client.NewClient(rawURL, username, password) + openErr := haystackClient.Open() if openErr != nil { + if insecureHost != "" { + unregisterInsecureHost(insecureHost) + } return nil, openErr } - datasource := Datasource{client: client} + datasource := Datasource{client: haystackClient, insecureHost: insecureHost} return &datasource, nil } // Datasource is an example datasource which can respond to data queries, reports // its health and has streaming skills. type Datasource struct { - client HaystackClient + client HaystackClient + insecureHost string } type Options struct { - Url string `json:"url"` - Username string `json:"username"` + Url string `json:"url"` + Username string `json:"username"` + SkipTlsVerify bool `json:"skipTlsVerify"` } // Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance // created. As soon as datasource settings change detected by SDK old datasource instance will // be disposed and a new one will be created using NewSampleDatasource factory function. func (datasource *Datasource) Dispose() { + if datasource.insecureHost != "" { + unregisterInsecureHost(datasource.insecureHost) + } datasource.client.Close() } diff --git a/pkg/plugin/transport.go b/pkg/plugin/transport.go new file mode 100644 index 0000000..ea48aac --- /dev/null +++ b/pkg/plugin/transport.go @@ -0,0 +1,62 @@ +package plugin + +import ( + "crypto/tls" + "net/http" + "sync" +) + +var ( + insecureHostsMu sync.RWMutex + insecureHostsCount = map[string]int{} + transportOnce sync.Once + originalTransport http.RoundTripper + insecureHTTPTransport http.RoundTripper +) + +// registerInsecureHost marks a URL host as one that should skip TLS verification. +// On the first call, it replaces http.DefaultTransport with a routing transport that +// directs insecure hosts to a TLS-skipping transport and all others to the original. +func registerInsecureHost(host string) { + transportOnce.Do(func() { + originalTransport = http.DefaultTransport + if dt, ok := originalTransport.(*http.Transport); ok { + clone := dt.Clone() + clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 + insecureHTTPTransport = clone + } else { + insecureHTTPTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402 + } + } + http.DefaultTransport = &routingTransport{} + }) + + insecureHostsMu.Lock() + defer insecureHostsMu.Unlock() + insecureHostsCount[host]++ +} + +// unregisterInsecureHost removes the host from the insecure set when its ref count reaches zero. +func unregisterInsecureHost(host string) { + insecureHostsMu.Lock() + defer insecureHostsMu.Unlock() + insecureHostsCount[host]-- + if insecureHostsCount[host] <= 0 { + delete(insecureHostsCount, host) + } +} + +// routingTransport routes HTTP requests to the appropriate transport based on the target host. +type routingTransport struct{} + +func (t *routingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + insecureHostsMu.RLock() + count := insecureHostsCount[req.URL.Host] + insecureHostsMu.RUnlock() + + if count > 0 { + return insecureHTTPTransport.RoundTrip(req) + } + return originalTransport.RoundTrip(req) +} diff --git a/src/components/ConfigEditor.tsx b/src/components/ConfigEditor.tsx index 129bbee..1536106 100644 --- a/src/components/ConfigEditor.tsx +++ b/src/components/ConfigEditor.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent } from 'react'; -import { InlineField, Input, SecretInput } from '@grafana/ui'; +import { InlineField, InlineSwitch, Input, SecretInput } from '@grafana/ui'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { HaystackDataSourceOptions, HaystackSecureJsonData } from '../types'; @@ -33,6 +33,10 @@ export function ConfigEditor(props: Props) { }); }; + const onSkipTlsVerifyChange = (event: ChangeEvent) => { + onOptionsChange({ ...options, jsonData: { ...options.jsonData, skipTlsVerify: event.target.checked } }); + }; + const onResetPassword = () => { onOptionsChange({ ...options, @@ -78,6 +82,9 @@ export function ConfigEditor(props: Props) { onChange={onPasswordChange} /> + + + ); } diff --git a/src/types.ts b/src/types.ts index 5167f34..98f60e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,7 @@ export const DEFAULT_QUERY: Partial = { export interface HaystackDataSourceOptions extends DataSourceJsonData { url: string; username: string; + skipTlsVerify?: boolean; } /**