mirror of
https://github.com/grafana/grafana.git
synced 2026-02-18 18:20:52 -05:00
Enable K8 Resources Endpoint again for Testing (#117876)
* Add some unit tests and enable sub resource so we can start testing manually on dev * add test to verify auth headers are stripped when being forwarding requests to client * add integration tests * update test packages * polish * fix tests * fix panic * fmt * white space * add feature flag to routing of resources endpoint * fix missing type * update integration tests with correct feature flag * fix test * update other integration tests to use ff
This commit is contained in:
parent
3bb480c74a
commit
e0b2ccb061
13 changed files with 1287 additions and 1570 deletions
|
|
@ -339,6 +339,11 @@ export interface FeatureToggles {
|
|||
*/
|
||||
datasourcesRerouteLegacyCRUDAPIs?: boolean;
|
||||
/**
|
||||
* Handle datasource resource requests to the legacy API routes by querying the new datasource api group endpoints behind the scenes.
|
||||
* @default false
|
||||
*/
|
||||
datasourcesApiServerEnableResourceEndpoint?: boolean;
|
||||
/**
|
||||
* Runs CloudWatch metrics queries as separate batches
|
||||
* @default false
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ func TestGetK8sDataSourceByUIDHandler(t *testing.T) {
|
|||
featuremgmt.FlagDatasourcesRerouteLegacyCRUDAPIs,
|
||||
featuremgmt.FlagQueryService,
|
||||
featuremgmt.FlagQueryServiceWithConnections,
|
||||
featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint,
|
||||
),
|
||||
dsConnectionClient: &mockConnectionClient{result: tt.connectionResult, err: tt.connectionErr},
|
||||
clientConfigProvider: &mockDirectRestConfigProvider{host: "http://localhost", transport: &mockRoundTripper{statusCode: tt.statusCode, responseBody: tt.responseBody}},
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ var (
|
|||
)
|
||||
|
||||
type DataSourceAPIBuilderConfig struct {
|
||||
LoadQueryTypes bool
|
||||
UseDualWriter bool
|
||||
LoadQueryTypes bool
|
||||
UseDualWriter bool
|
||||
EnableResourceEndpoint bool
|
||||
}
|
||||
|
||||
// DataSourceAPIBuilder is used just so wire has something unique to return
|
||||
|
|
@ -106,8 +107,9 @@ func RegisterAPIService(
|
|||
accessControl,
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
DataSourceAPIBuilderConfig{
|
||||
LoadQueryTypes: features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
||||
UseDualWriter: features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections),
|
||||
LoadQueryTypes: features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
||||
UseDualWriter: features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections),
|
||||
EnableResourceEndpoint: features.IsEnabledGlobally(featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -246,7 +248,10 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
|||
ds := b.datasourceResourceInfo
|
||||
storage[ds.StoragePath("query")] = &subQueryREST{builder: b}
|
||||
storage[ds.StoragePath("health")] = &subHealthREST{builder: b}
|
||||
storage[ds.StoragePath("resource")] = &subResourceREST{builder: b}
|
||||
|
||||
if b.cfg.EnableResourceEndpoint {
|
||||
storage[ds.StoragePath("resource")] = &subResourceREST{builder: b}
|
||||
}
|
||||
|
||||
// FIXME: temporarily register both "datasources" and "connections" query paths
|
||||
// This lets us deploy both datasources/{uid}/query and connections/{uid}/query
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@ package datasource
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/plugins/httpresponsesender"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
type subResourceREST struct {
|
||||
|
|
@ -47,37 +48,30 @@ func (r *subResourceREST) NewConnectOptions() (runtime.Object, bool, string) {
|
|||
return nil, true, ""
|
||||
}
|
||||
|
||||
// FIXME: this endpoint has not been tested yet, so it is not enabled by default.
|
||||
// It is especially important to make sure the `ClearAuthHeadersMiddleware` is active,
|
||||
// when using this endpoint.
|
||||
var resourceEnabled = false
|
||||
|
||||
func (r *subResourceREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
if !resourceEnabled {
|
||||
return nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pluginCtx, err := r.builder.getPluginContext(ctx, name)
|
||||
if err != nil {
|
||||
backend.Logger.Error("failed to get plugin context for datasource in resource handler", "name", name, "error", err)
|
||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
||||
return nil, r.builder.datasourceResourceInfo.NewNotFound(name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
clonedReq, err := resourceRequest(req)
|
||||
if err != nil {
|
||||
backend.Logger.Error("failed to create resource request", "error", err)
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
backend.Logger.Error("failed to read request body", "error", err)
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
|
@ -92,6 +86,7 @@ func (r *subResourceREST) Connect(ctx context.Context, name string, opts runtime
|
|||
}, httpresponsesender.New(w))
|
||||
|
||||
if err != nil {
|
||||
backend.Logger.Error("plugin resource request failed", "error", err)
|
||||
responder.Error(err)
|
||||
}
|
||||
}), nil
|
||||
|
|
|
|||
|
|
@ -1,13 +1,95 @@
|
|||
package datasource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
)
|
||||
|
||||
type resourceMockClient struct {
|
||||
callResourceFunc func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error
|
||||
}
|
||||
|
||||
func (m *resourceMockClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
if m.callResourceFunc != nil {
|
||||
return m.callResourceFunc(ctx, req, sender)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resourceMockClient) QueryChunkedData(ctx context.Context, req *backend.QueryChunkedDataRequest, w backend.ChunkedDataWriter) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resourceMockClient) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type resourceMockDatasourceProvider struct {
|
||||
instanceSettings *backend.DataSourceInstanceSettings
|
||||
instanceSettingsErr error
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) CreateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) UpdateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) DeleteDataSource(ctx context.Context, uid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resourceMockDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) {
|
||||
return m.instanceSettings, m.instanceSettingsErr
|
||||
}
|
||||
|
||||
type resourceMockContextProvider struct {
|
||||
pluginCtx backend.PluginContext
|
||||
pluginCtxErr error
|
||||
}
|
||||
|
||||
func (m *resourceMockContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) {
|
||||
return m.pluginCtx, m.pluginCtxErr
|
||||
}
|
||||
|
||||
type resourceMockResponder struct {
|
||||
lastErr error
|
||||
}
|
||||
|
||||
func (m *resourceMockResponder) Object(statusCode int, obj runtime.Object) {}
|
||||
|
||||
func (m *resourceMockResponder) Error(err error) {
|
||||
m.lastErr = err
|
||||
}
|
||||
|
||||
func TestResourceRequest(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
|
@ -69,3 +151,304 @@ func TestResourceRequest(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubResourceREST_Connect(t *testing.T) {
|
||||
t.Run("returns error when GetInstanceSettings fails", func(t *testing.T) {
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettingsErr: errors.New("datasource not found"),
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
responder := &resourceMockResponder{}
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, responder)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Nil(t, handler)
|
||||
require.Contains(t, err.Error(), "datasource not found")
|
||||
})
|
||||
|
||||
t.Run("returns error when PluginContextForDataSource fails", func(t *testing.T) {
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{
|
||||
UID: "test-ds",
|
||||
Name: "Test Datasource",
|
||||
},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtxErr: errors.New("failed to create plugin context"),
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
responder := &resourceMockResponder{}
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, responder)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Nil(t, handler)
|
||||
require.Contains(t, err.Error(), "failed to create plugin context")
|
||||
})
|
||||
|
||||
t.Run("successfully creates handler and forwards request", func(t *testing.T) {
|
||||
var capturedRequest *backend.CallResourceRequest
|
||||
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
capturedRequest = req
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
Headers: map[string][]string{"Content-Type": {"application/json"}},
|
||||
Body: []byte(`{"message": "success"}`),
|
||||
})
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{
|
||||
UID: "test-ds",
|
||||
Name: "Test Datasource",
|
||||
},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
UID: "test-ds",
|
||||
Name: "Test Datasource",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
responder := &resourceMockResponder{}
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, responder)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
reqBody := []byte(`{"test": "data"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://localhost/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-ds/resource/some/path?key=value", bytes.NewReader(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Custom-Header", "custom-value")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
require.NotNil(t, capturedRequest)
|
||||
require.Equal(t, http.MethodPost, capturedRequest.Method)
|
||||
require.Equal(t, "some/path", capturedRequest.Path)
|
||||
require.Equal(t, "some/path?key=value", capturedRequest.URL)
|
||||
require.Equal(t, reqBody, capturedRequest.Body)
|
||||
require.Contains(t, capturedRequest.Headers["Content-Type"], "application/json")
|
||||
require.Contains(t, capturedRequest.Headers["X-Custom-Header"], "custom-value")
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Equal(t, `{"message": "success"}`, recorder.Body.String())
|
||||
})
|
||||
|
||||
t.Run("forwards all HTTP methods correctly", func(t *testing.T) {
|
||||
methods := []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
var capturedMethod string
|
||||
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
capturedMethod = req.Method
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: []byte("ok"),
|
||||
})
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{UID: "test-ds"},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, &resourceMockResponder{})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(method, "http://localhost/apis/test/resource/path", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
require.Equal(t, method, capturedMethod)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles CallResource error", func(t *testing.T) {
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return errors.New("plugin error")
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{UID: "test-ds"},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
responder := &resourceMockResponder{}
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, responder)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://localhost/apis/test/resource/path", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
// The responder should have received the error
|
||||
require.NotNil(t, responder.lastErr)
|
||||
require.Contains(t, responder.lastErr.Error(), "plugin error")
|
||||
})
|
||||
|
||||
t.Run("forwards request body to plugin", func(t *testing.T) {
|
||||
var capturedBody []byte
|
||||
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
capturedBody = req.Body
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{UID: "test-ds"},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, &resourceMockResponder{})
|
||||
require.NoError(t, err)
|
||||
|
||||
requestBody := `{"key": "value", "nested": {"foo": "bar"}}`
|
||||
req := httptest.NewRequest(http.MethodPost, "http://localhost/apis/test/resource/path", bytes.NewBufferString(requestBody))
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
require.Equal(t, requestBody, string(capturedBody))
|
||||
})
|
||||
|
||||
t.Run("handles empty request body", func(t *testing.T) {
|
||||
var capturedBody []byte
|
||||
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
capturedBody = req.Body
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{UID: "test-ds"},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, &resourceMockResponder{})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://localhost/apis/test/resource/path", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
require.Empty(t, capturedBody)
|
||||
})
|
||||
|
||||
t.Run("preserves query parameters in URL", func(t *testing.T) {
|
||||
var capturedURL string
|
||||
var capturedPath string
|
||||
|
||||
mockClient := &resourceMockClient{
|
||||
callResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
capturedURL = req.URL
|
||||
capturedPath = req.Path
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
},
|
||||
}
|
||||
mockProvider := &resourceMockDatasourceProvider{
|
||||
instanceSettings: &backend.DataSourceInstanceSettings{UID: "test-ds"},
|
||||
}
|
||||
mockContext := &resourceMockContextProvider{
|
||||
pluginCtx: backend.PluginContext{},
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
client: mockClient,
|
||||
datasources: mockProvider,
|
||||
contextProvider: mockContext,
|
||||
}
|
||||
r := &subResourceREST{builder: builder}
|
||||
|
||||
handler, err := r.Connect(context.Background(), "test-ds", nil, &resourceMockResponder{})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://localhost/apis/test/resource/api/endpoint?foo=bar&baz=qux", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
require.Equal(t, "api/endpoint", capturedPath)
|
||||
require.Equal(t, "api/endpoint?foo=bar&baz=qux", capturedURL)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -516,6 +516,14 @@ var (
|
|||
RequiresRestart: false,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "datasourcesApiServerEnableResourceEndpoint",
|
||||
Description: "Handle datasource resource requests to the legacy API routes by querying the new datasource api group endpoints behind the scenes.",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDatasourcesCoreServicesSquad,
|
||||
RequiresRestart: false,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "cloudWatchBatchQueries",
|
||||
Description: "Runs CloudWatch metrics queries as separate batches",
|
||||
|
|
|
|||
103
pkg/services/featuremgmt/toggles_gen.csv
generated
103
pkg/services/featuremgmt/toggles_gen.csv
generated
|
|
@ -13,7 +13,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2024-10-23,lokiShardSplitting,experimental,@grafana/observability-logs,false,false,true
|
||||
2023-02-09,lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true
|
||||
2022-02-09,influxdbBackendMigration,GA,@grafana/partner-datasources,false,false,true
|
||||
2026-01-19,liveAPIServer,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
2026-02-18,liveAPIServer,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
2025-08-29,starsFromAPIServer,experimental,@grafana/grafana-frontend-navigation,false,false,true
|
||||
2025-08-29,kubernetesStars,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2023-11-29,influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false
|
||||
|
|
@ -24,7 +24,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2023-06-06,refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false
|
||||
2023-05-05,faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true
|
||||
2023-04-24,enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,false
|
||||
2026-02-10,faroSessionReplay,experimental,@grafana/session-replay,false,false,true
|
||||
2026-02-18,faroSessionReplay,experimental,@grafana/session-replay,false,false,true
|
||||
2023-07-12,logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true
|
||||
2023-07-06,awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false
|
||||
2023-07-13,mlExpressions,experimental,@grafana/alerting-squad,false,false,false
|
||||
|
|
@ -42,11 +42,11 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2023-09-19,lokiRunQueriesInParallel,privatePreview,@grafana/oss-big-tent,false,false,false
|
||||
2023-09-28,externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
|
||||
2023-12-05,kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2025-06-25,kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2025-06-26,kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2024-06-05,kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,false
|
||||
2025-07-31,kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2025-08-01,kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2025-08-29,useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true
|
||||
2025-07-31,kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false
|
||||
2025-08-01,kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false
|
||||
2025-08-29,kubernetesCorrelations,experimental,@grafana/datapro,false,true,false
|
||||
2025-12-09,kubernetesUnifiedStorageQuotas,experimental,@grafana/search-and-storage,false,true,false
|
||||
2025-10-16,kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false
|
||||
|
|
@ -56,13 +56,14 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-04-11,dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
2025-07-30,scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false
|
||||
2024-05-23,datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2026-01-22,datasourceDisableIdApi,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||
2026-02-18,datasourceDisableIdApi,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||
2024-04-19,queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||
2025-08-28,queryServiceWithConnections,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||
2026-02-13,useNewAPIsForDatasourceCRUD,experimental,@grafana/grafana-datasources-core-services,false,true,true
|
||||
2026-02-18,useNewAPIsForDatasourceCRUD,experimental,@grafana/grafana-datasources-core-services,false,true,true
|
||||
2024-04-19,queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||
2024-04-19,queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
|
||||
2026-01-19,datasourcesRerouteLegacyCRUDAPIs,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
2026-02-18,datasourcesRerouteLegacyCRUDAPIs,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
2026-02-18,datasourcesApiServerEnableResourceEndpoint,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
2023-10-20,cloudWatchBatchQueries,GA,@grafana/aws-datasources,false,false,false
|
||||
2023-10-12,cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
2023-10-30,alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false
|
||||
|
|
@ -81,7 +82,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-12-08,perPanelFiltering,experimental,@grafana/dashboards-squad,false,false,true
|
||||
2023-11-03,panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
|
||||
2023-11-06,pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
2026-02-16,reportingV2Layouts,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
2026-02-18,reportingV2Layouts,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
2024-01-02,canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
2025-07-24,timeComparison,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2023-12-13,tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
|
||||
|
|
@ -89,8 +90,8 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2024-01-10,alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false
|
||||
2024-01-18,jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
|
||||
2025-12-29,auditLoggingAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,true,false
|
||||
2025-07-31,secretsManagementAppPlatformUI,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
2026-02-04,secretsKeeperUI,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
2025-08-01,secretsManagementAppPlatformUI,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
2026-02-18,secretsKeeperUI,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
2024-01-23,alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
|
||||
2025-01-27,alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
|
||||
2024-11-27,scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
|
|
@ -111,16 +112,16 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2024-04-16,disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
|
||||
2024-04-22,grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
2022-10-07,queryLibrary,preview,@grafana/sharing-squad,false,false,false
|
||||
2026-01-19,savedQueriesRBAC,preview,@grafana/sharing-squad,false,false,false
|
||||
2026-02-18,savedQueriesRBAC,preview,@grafana/sharing-squad,false,false,false
|
||||
2025-08-29,dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
|
||||
2025-11-07,suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
|
||||
2026-01-21,dashboardValidatorApp,experimental,@grafana/sharing-squad,false,false,false
|
||||
2026-02-18,dashboardValidatorApp,experimental,@grafana/sharing-squad,false,false,false
|
||||
2025-10-28,dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
|
||||
2024-05-24,alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
2026-01-14,alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-12-19,alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
||||
2024-05-23,alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-06,alertingSyncNotifiersApiMigration,experimental,@grafana/alerting-squad,false,false,true
|
||||
2026-02-18,alertingSyncNotifiersApiMigration,experimental,@grafana/alerting-squad,false,false,true
|
||||
2024-05-27,preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
2024-05-29,alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,false
|
||||
2024-06-05,pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
|
|
@ -135,7 +136,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2024-09-11,alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false
|
||||
2024-08-09,dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
2024-08-30,newFiltersUI,GA,@grafana/dashboards-squad,false,false,false
|
||||
2025-07-31,vizActionsAuth,preview,@grafana/dataviz-squad,false,false,true
|
||||
2025-08-01,vizActionsAuth,preview,@grafana/dataviz-squad,false,false,true
|
||||
2024-09-27,alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true
|
||||
2024-08-29,exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true
|
||||
2024-08-29,exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true
|
||||
|
|
@ -164,7 +165,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-07-23,alertingAIFeedback,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-16,alertingAIImproveAlertRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-16,alertingAIGenTemplates,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-31,alertingEnrichmentPerRule,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-08-01,alertingEnrichmentPerRule,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-08-29,alertingEnrichmentAssistantInvestigations,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-16,alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false
|
||||
2024-11-22,alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
|
|
@ -174,7 +175,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2024-12-27,k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
||||
2025-01-09,improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false
|
||||
2025-05-22,teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-05,teamHttpHeadersFromAppPlatform,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,teamHttpHeadersFromAppPlatform,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-01-20,grafanaAdvisor,privatePreview,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-01-15,elasticsearchImprovedParsing,experimental,@grafana/partner-datasources,false,false,false
|
||||
2025-01-21,datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true
|
||||
|
|
@ -194,7 +195,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-03-14,infinityRunQueriesInParallel,privatePreview,@grafana/oss-big-tent,false,false,false
|
||||
2025-03-14,alertingMigrationUI,GA,@grafana/alerting-squad,false,false,true
|
||||
2025-05-21,alertingImportYAMLUI,GA,@grafana/alerting-squad,false,false,true
|
||||
2026-01-27,alertingMigrationWizardUI,experimental,@grafana/alerting-squad,false,false,true
|
||||
2026-02-18,alertingMigrationWizardUI,experimental,@grafana/alerting-squad,false,false,true
|
||||
2025-04-02,azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,false
|
||||
2025-03-31,localeFormatPreference,deprecated,@grafana/grafana-frontend-platform,false,false,false
|
||||
2025-03-21,unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false
|
||||
|
|
@ -204,7 +205,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-04-09,unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-04-07,logsPanelControls,preview,@grafana/observability-logs,false,false,true
|
||||
2025-04-09,metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||
2025-07-31,grafanaAssistantInProfilesDrilldown,GA,@grafana/observability-traces-and-profiling,false,false,true
|
||||
2025-08-01,grafanaAssistantInProfilesDrilldown,GA,@grafana/observability-traces-and-profiling,false,false,true
|
||||
2025-07-15,tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
||||
2025-04-16,pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-04-22,alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
|
|
@ -212,14 +213,14 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-04-24,alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
|
||||
2025-06-18,kubernetesAuthzApis,deprecated,@grafana/identity-access-team,false,false,false
|
||||
2025-08-29,kubernetesAuthZHandlerRedirect,deprecated,@grafana/identity-access-team,false,false,false
|
||||
2026-02-04,kubernetesAuthZResourcePermissionsRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-04,kubernetesAuthZRolesRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-07-31,kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,kubernetesAuthZResourcePermissionsRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,kubernetesAuthZRolesRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-08-01,kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-10-13,kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-12,kubernetesAuthzCoreRolesApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-15,kubernetesAuthzGlobalRolesApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-12,kubernetesAuthzRolesApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-14,kubernetesAuthzTeamLBACRuleApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,kubernetesAuthzTeamLBACRuleApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-12,kubernetesAuthzRoleBindingsApi,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-07-25,kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-11-25,kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false
|
||||
|
|
@ -227,11 +228,11 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-12-10,recentlyViewedDashboards,experimental,@grafana/grafana-frontend-navigation,false,false,true
|
||||
2026-01-13,experimentRecentlyViewedDashboards,experimental,@grafana/grafana-frontend-navigation,false,false,true
|
||||
2025-06-06,alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-31,alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-31,alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-08-01,alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-08-01,alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-06-10,alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-31,alertingImportAlertmanagerUI,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-01-26,alertingDisableDMAinUI,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-08-01,alertingImportAlertmanagerUI,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,alertingDisableDMAinUI,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-15,sharingDashboardImage,GA,@grafana/sharing-squad,false,false,true
|
||||
2025-06-17,preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false
|
||||
2025-06-24,tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false
|
||||
|
|
@ -244,33 +245,33 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2025-07-18,unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false
|
||||
2025-07-31,dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true
|
||||
2025-07-25,alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false
|
||||
2025-07-31,restrictedPluginApis,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-07-31,favoriteDatasources,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-07-31,newLogContext,experimental,@grafana/observability-logs,false,false,true
|
||||
2025-07-31,newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
2025-07-31,teamFolders,experimental,@grafana/grafana-frontend-navigation,false,false,false
|
||||
2025-08-01,restrictedPluginApis,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-08-01,favoriteDatasources,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-08-01,newLogContext,experimental,@grafana/observability-logs,false,false,true
|
||||
2025-08-01,newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
2025-08-01,teamFolders,experimental,@grafana/grafana-frontend-navigation,false,false,false
|
||||
2025-10-22,interactiveLearning,preview,@grafana/pathfinder,false,false,false
|
||||
2025-07-31,alertingTriage,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-12,alertingAlertsActivityBanner,experimental,@grafana/alerting-squad,false,false,true
|
||||
2025-07-31,graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
2025-07-31,azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true
|
||||
2025-07-31,prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false
|
||||
2025-07-31,pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,false
|
||||
2025-08-01,alertingTriage,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,alertingAlertsActivityBanner,experimental,@grafana/alerting-squad,false,false,true
|
||||
2025-08-01,graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
2025-08-01,azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true
|
||||
2025-08-01,prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false
|
||||
2025-08-01,pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,false
|
||||
2025-08-29,cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-08-29,cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-10-24,pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-10-20,newGauge,preview,@grafana/dataviz-squad,false,false,true
|
||||
2025-11-12,newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true
|
||||
2026-01-28,panelStyleActions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2026-02-09,vizPresets,preview,@grafana/dataviz-squad,false,false,true
|
||||
2025-12-01,externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2026-02-18,panelStyleActions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2026-02-18,vizPresets,preview,@grafana/dataviz-squad,false,false,true
|
||||
2025-12-02,externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2025-12-18,heatmapRowsAxisOptions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
2025-10-17,preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
|
||||
2025-10-31,jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false
|
||||
2025-10-17,pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
2025-11-12,newPanelPadding,preview,@grafana/dashboards-squad,false,false,true
|
||||
2025-10-20,onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
|
||||
2026-02-06,excludeRedundantManagedPermissions,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,excludeRedundantManagedPermissions,experimental,@grafana/identity-access-team,false,false,false
|
||||
2025-12-16,pluginInsights,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
2025-10-29,panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
|
||||
2025-12-15,elasticsearchRawDSLQuery,experimental,@grafana/partner-datasources,false,false,false
|
||||
|
|
@ -288,13 +289,13 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
|
|||
2026-01-06,secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
2026-01-07,profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
||||
2026-01-14,alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false
|
||||
2026-01-15,queryWithAssistant,experimental,@grafana/oss-big-tent,false,false,true
|
||||
2026-01-13,queryEditorNext,experimental,@grafana/datapro,false,false,true
|
||||
2026-01-20,kubernetesTeamBindings,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-04,kubernetesTeamsHandlerRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-09,kubernetesTeamSync,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-01-27,alertingMultiplePolicies,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-11,alertingIgnorePendingForNoDataAndError,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-11,alertingNotificationHistoryRuleViewer,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-13,alertingNotificationHistoryGlobal,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,queryWithAssistant,experimental,@grafana/oss-big-tent,false,false,true
|
||||
2026-02-18,queryEditorNext,experimental,@grafana/datapro,false,false,true
|
||||
2026-02-18,kubernetesTeamBindings,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,kubernetesTeamsHandlerRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,kubernetesTeamSync,experimental,@grafana/identity-access-team,false,false,false
|
||||
2026-02-18,alertingMultiplePolicies,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,alertingIgnorePendingForNoDataAndError,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,alertingNotificationHistoryRuleViewer,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,alertingNotificationHistoryGlobal,experimental,@grafana/alerting-squad,false,false,false
|
||||
2026-02-18,react19,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
|
|
|
|||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
|
|
@ -203,6 +203,10 @@ const (
|
|||
// Handle datasource CRUD requests to the legacy API routes by querying the new datasource api group endpoints behind the scenes.
|
||||
FlagDatasourcesRerouteLegacyCRUDAPIs = "datasourcesRerouteLegacyCRUDAPIs"
|
||||
|
||||
// FlagDatasourcesApiServerEnableResourceEndpoint
|
||||
// Handle datasource resource requests to the legacy API routes by querying the new datasource api group endpoints behind the scenes.
|
||||
FlagDatasourcesApiServerEnableResourceEndpoint = "datasourcesApiServerEnableResourceEndpoint"
|
||||
|
||||
// FlagCloudWatchBatchQueries
|
||||
// Runs CloudWatch metrics queries as separate batches
|
||||
FlagCloudWatchBatchQueries = "cloudWatchBatchQueries"
|
||||
|
|
|
|||
2007
pkg/services/featuremgmt/toggles_gen.json
generated
2007
pkg/services/featuremgmt/toggles_gen.json
generated
File diff suppressed because it is too large
Load diff
284
pkg/tests/apis/datasource/resources_test.go
Normal file
284
pkg/tests/apis/datasource/resources_test.go
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
// testdataResponse represents the JSON response from /test/json endpoint for grafana-test-dataosurce
|
||||
type testdataResponse struct {
|
||||
Message string `json:"message"`
|
||||
Request struct {
|
||||
Method string `json:"method"`
|
||||
URL url.URL `json:"url"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
Body map[string]any `json:"body"`
|
||||
} `json:"request"`
|
||||
}
|
||||
|
||||
func setup(t *testing.T) *apis.K8sTestHelper {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint, // enables resource endpoint
|
||||
},
|
||||
})
|
||||
t.Cleanup(helper.Shutdown)
|
||||
|
||||
ctx := context.Background()
|
||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||
Group: "grafana-testdata-datasource.datasource.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "datasources",
|
||||
}).Namespace("default")
|
||||
|
||||
_, err := client.Create(ctx, &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
"kind": "DataSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-resource",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Resource Datasource",
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create datasource: %v", err)
|
||||
}
|
||||
|
||||
return helper
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceResources(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
helper := setup(t)
|
||||
|
||||
t.Run("GET resource endpoint returns testdata response", func(t *testing.T) {
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode, "expected OK status, got body: %s", string(raw.Body))
|
||||
require.Contains(t, string(raw.Body), "Hello world", "expected testdata greeting response")
|
||||
})
|
||||
|
||||
t.Run("GET /test/json returns JSON with request echo", func(t *testing.T) {
|
||||
helper := setup(t)
|
||||
|
||||
raw := apis.DoRequest[testdataResponse](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/json",
|
||||
}, &testdataResponse{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.NotNil(t, raw.Result)
|
||||
require.Equal(t, "Hello world from test datasource!", raw.Result.Message)
|
||||
require.Equal(t, "GET", raw.Result.Request.Method)
|
||||
})
|
||||
|
||||
t.Run("POST with body echoes request body", func(t *testing.T) {
|
||||
raw := apis.DoRequest[testdataResponse](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodPost,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/json",
|
||||
Body: []byte(`{"foo": "bar", "count": 42}`),
|
||||
}, &testdataResponse{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.NotNil(t, raw.Result)
|
||||
require.Equal(t, "POST", raw.Result.Request.Method)
|
||||
require.NotNil(t, raw.Result.Request.Body)
|
||||
require.Equal(t, "bar", raw.Result.Request.Body["foo"])
|
||||
require.Equal(t, float64(42), raw.Result.Request.Body["count"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceResourceHeaders(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
helper := setup(t)
|
||||
t.Run("auth headers are stripped before request reaches plugin", func(t *testing.T) {
|
||||
raw := apis.DoRequest[testdataResponse](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodPost,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/json",
|
||||
Headers: map[string]string{
|
||||
"X-Custom-Header": "custom-value",
|
||||
},
|
||||
ContentType: "application/json",
|
||||
Accept: "application/json",
|
||||
Body: []byte(`{"foo": "bar", "count": 42}`),
|
||||
}, &testdataResponse{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.NotNil(t, raw.Result)
|
||||
|
||||
// ClearAuthHeaders middleware should strip auth headers before request is sent to plugin
|
||||
authHeaders := raw.Result.Request.Headers["Authorization"]
|
||||
require.Empty(t, authHeaders, "Authorization header should be stripped before request reaches plugin")
|
||||
deviceIDHeaders := raw.Result.Request.Headers["X-Grafana-Device-Id"]
|
||||
require.Empty(t, deviceIDHeaders, "X-Grafana-Device-Id header should be stripped before request reaches plugin")
|
||||
// Non-auth headers must still be forwarded
|
||||
customHeaders := raw.Result.Request.Headers["X-Custom-Header"]
|
||||
require.NotEmpty(t, customHeaders)
|
||||
require.Equal(t, "custom-value", customHeaders[0])
|
||||
})
|
||||
|
||||
t.Run("query parameters are forwarded to plugin", func(t *testing.T) {
|
||||
raw := apis.DoRequest[testdataResponse](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/json?param1=value1¶m2=value2",
|
||||
}, &testdataResponse{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.NotNil(t, raw.Result)
|
||||
|
||||
// parse raw.Body to testdataResponse
|
||||
testdataResponse := &testdataResponse{}
|
||||
err := json.Unmarshal(raw.Body, testdataResponse)
|
||||
require.NoError(t, err, "failed to unmarshal response body: %s", string(raw.Body))
|
||||
|
||||
require.Contains(t, testdataResponse.Request.URL.RawQuery, "param1=value1", "URL in plugin should contain query parameters")
|
||||
require.Contains(t, testdataResponse.Request.URL.RawQuery, "param2=value2", "URL in plugin should contain query parameters")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceStreamingResource(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
helper := setup(t)
|
||||
|
||||
t.Run("GET streaming resource returns expected response", func(t *testing.T) {
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/stream",
|
||||
}, nil)
|
||||
|
||||
require.NotNil(t, raw.Response.StatusCode)
|
||||
require.Equal(t, int(http.StatusOK), raw.Response.StatusCode)
|
||||
require.Contains(t, string(raw.Body), "Hello world from test datasource!")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceResourceAuthorization(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
helper := setup(t)
|
||||
|
||||
t.Run("resource endpoint returns 404 for non-existent datasource", func(t *testing.T) {
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/does-not-exist/resource",
|
||||
}, nil)
|
||||
|
||||
require.NotNil(t, raw.Status)
|
||||
require.Equal(t, int32(http.StatusNotFound), raw.Status.Code)
|
||||
})
|
||||
|
||||
t.Run("resource endpoint requires authentication", func(t *testing.T) {
|
||||
// None role should not be able to access resource endpoint for GET
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.None,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource",
|
||||
}, nil)
|
||||
|
||||
require.NotNil(t, raw.Response)
|
||||
require.Equal(t, http.StatusForbidden, raw.Response.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("resource endpoint cross-org access denied", func(t *testing.T) {
|
||||
// OrgB user should not be able to access Org1's datasource resources
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.OrgB.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource",
|
||||
}, nil)
|
||||
|
||||
require.NotNil(t, raw.Status)
|
||||
require.Equal(t, int32(http.StatusForbidden), raw.Status.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceResourcesMethods(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
helper := setup(t)
|
||||
|
||||
httpMethods := []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
}
|
||||
|
||||
for _, method := range httpMethods {
|
||||
t.Run(fmt.Sprintf("%s method is forwarded correctly", method), func(t *testing.T) {
|
||||
var body []byte
|
||||
if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch {
|
||||
body = []byte(`{"test": "data"}`)
|
||||
}
|
||||
|
||||
raw := apis.DoRequest[testdataResponse](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: method,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/test/json",
|
||||
Body: body,
|
||||
}, &testdataResponse{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode,
|
||||
"method %s should return OK, got: %s", method, string(raw.Body))
|
||||
require.NotNil(t, raw.Result)
|
||||
require.Equal(t, method, raw.Result.Request.Method,
|
||||
"echoed method should match request method")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDatasourceResourcesScenarios(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := setup(t)
|
||||
|
||||
t.Run("GET /scenarios returns list of available scenarios", func(t *testing.T) {
|
||||
raw := apis.DoRequest[[]map[string]any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: http.MethodGet,
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test-resource/resource/scenarios",
|
||||
}, &[]map[string]any{})
|
||||
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.NotNil(t, raw.Result)
|
||||
|
||||
// Should return a list of scenarios
|
||||
scenarios := *raw.Result
|
||||
require.NotEmpty(t, scenarios, "should return at least one scenario")
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
require.NotEmpty(t, scenario["id"], "scenario should have id")
|
||||
require.NotEmpty(t, scenario["name"], "scenario should have name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -40,8 +40,9 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
|||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint, // enables resource endpoint
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"datasources.grafana-testdata-datasource.datasource.grafana.app": {
|
||||
|
|
@ -198,11 +199,8 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
|||
Method: "GET",
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource",
|
||||
}, nil)
|
||||
// endpoint is disabled currently because it has not been
|
||||
// sufficiently tested.
|
||||
// for more info see pkg/registry/apis/datasource/sub_resource.go
|
||||
require.Equal(t, int32(501), raw.Status.Code)
|
||||
// require.Equal(t, `Hello world from test datasource!`, string(raw.Body))
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
require.Contains(t, string(raw.Body), "Hello world from test datasource!")
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
|
|
@ -222,8 +220,9 @@ func TestIntegrationTestDatasourceAccess(t *testing.T) {
|
|||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint, // enables resource endpoint
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"datasources.grafana-testdata-datasource.datasource.grafana.app": {
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ type RequestParams struct {
|
|||
Body []byte
|
||||
ContentType string
|
||||
Accept string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
type K8sResponse[T any] struct {
|
||||
|
|
@ -571,6 +572,9 @@ func DoRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResp
|
|||
if params.Accept != "" {
|
||||
req.Header.Set("Accept", params.Accept)
|
||||
}
|
||||
for k, v := range params.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
rsp, err := sharedHTTPClient.Do(req)
|
||||
require.NoError(c.t, err)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
|||
featuremgmt.FlagKubernetesAlertingRules,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // library panels in v0
|
||||
featuremgmt.FlagQueryServiceWithConnections,
|
||||
featuremgmt.FlagDatasourcesApiServerEnableResourceEndpoint,
|
||||
featuremgmt.FlagKubernetesShortURLs,
|
||||
featuremgmt.FlagKubernetesCorrelations,
|
||||
featuremgmt.FlagKubernetesAlertingHistorian,
|
||||
|
|
|
|||
Loading…
Reference in a new issue