This commit is contained in:
Ryan McKinley
2026-01-12 14:22:07 +03:00
parent aae69c1e75
commit 0e6ad2e7c8
3 changed files with 178 additions and 99 deletions
@@ -147,6 +147,7 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
}
secureChanges[k] = v
dualwrite.SetUpdatedSecureValues(ctx, ds.Secure)
continue
}
// The legacy store must use fixed names generated by the internal system
@@ -420,6 +420,14 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat
}
}
// Propagate secure values from the update request to the unified storage update.
if secure := getUpdatedSecureValues(ctx); secure != nil {
wrapped, ok := unifiedInfo.(*wrappedUpdateInfo)
if ok {
wrapped.updatedSecureValues = secure
}
}
if d.readUnified {
return d.unified.Update(ctx, name, unifiedInfo, createValidation, updateValidation, unifiedForceCreate, options)
} else if d.errorIsOK {
+169 -99
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"testing"
"github.com/stretchr/testify/require"
@@ -13,9 +15,11 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
@@ -29,111 +33,177 @@ func TestMain(m *testing.M) {
func TestIntegrationTestDatasource(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // dev mode required for datasource connections
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
},
})
for _, mode := range []grafanarest.DualWriterMode{
// grafanarest.Mode0, // Legacy only
// grafanarest.Mode2, // write both, read legacy
// grafanarest.Mode3, // write both, read unified
grafanarest.Mode5, // Unified only
} {
t.Run(fmt.Sprintf("testdata (mode:%d)", mode), func(t *testing.T) {
ctx := context.Background()
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
},
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"datasources.testdata.datasource.grafana.app": {
DualWriterMode: mode,
},
},
})
// Create a single datasource
ds := helper.CreateDS(&datasources.AddDataSourceCommand{
Name: "test",
Type: datasources.DS_TESTDATA,
UID: "test",
OrgID: int64(1),
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
// These settings are not actually used, but testing that they get saved
Database: "testdb",
URL: "http://fake.url",
Access: datasources.DS_ACCESS_PROXY,
User: "example",
ReadOnly: true,
JsonData: simplejson.NewFromAny(map[string]any{
"hello": "world",
}),
SecureJsonData: map[string]string{
"aaa": "AAA",
"bbb": "BBB",
},
})
require.Equal(t, "test", ds.UID)
// 1. CREATE
out, err := client.Create(ctx, &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "testdata.datasource.grafana.app/v0alpha1",
"kind": "DataSource",
"metadata": map[string]any{
"name": "test",
},
"spec": map[string]any{
"title": "test",
},
"secure": map[string]any{
"aaa": map[string]any{
"create": "AAA",
},
"bbb": map[string]any{
"create": "BBB",
},
},
},
}, metav1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, "test", out.GetName())
t.Run("Admin configs", func(t *testing.T) {
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
ctx := context.Background()
obj, err := utils.MetaAccessor(out)
require.NoError(t, err)
list, err := client.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1, "expected a single connection")
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
secure, err := obj.GetSecureValues()
require.NoError(t, err)
spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec")
jj, _ := json.MarshalIndent(spec, "", " ")
fmt.Printf("%s\n", string(jj))
require.JSONEq(t, `{
"access": "proxy",
"database": "testdb",
"isDefault": true,
"jsonData": {
"hello": "world"
},
"readOnly": true,
"title": "test",
"url": "http://fake.url",
"user": "example"
}`, string(jj))
})
keys := slices.Collect(maps.Keys(secure))
require.ElementsMatch(t, []string{"aaa", "bbb"}, keys)
t.Run("Call subresources", func(t *testing.T) {
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
ctx := context.Background()
// 2. UPDATE
out, err = client.Update(ctx, &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "testdata.datasource.grafana.app/v0alpha1",
"metadata": map[string]any{
"name": "test",
},
"spec": map[string]any{
"title": "test",
"database": "testdb",
"url": "http://fake.url",
"access": datasources.DS_ACCESS_PROXY,
"user": "example",
"isDefault": true,
"readOnly": true,
"jsonData": map[string]any{
"hello": "world",
},
},
"secure": map[string]any{
"aaa": map[string]any{
"remove": true, // remove the first secure value
},
"ccc": map[string]any{
"create": "CCC", // add a third value
},
},
},
}, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, "test", out.GetName())
list, err := client.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1, "expected a single connection")
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
obj, err = utils.MetaAccessor(out)
require.NoError(t, err)
_, err = client.Get(ctx, "test", metav1.GetOptions{}, "health")
// endpoint is disabled currently because it has not been
// sufficiently tested.
// for more info see pkg/registry/apis/datasource/sub_health.go
require.Error(t, err)
var statusErr *apierrors.StatusError
require.True(t, errors.As(err, &statusErr))
require.Equal(t, int32(501), statusErr.ErrStatus.Code)
// require.NoError(t, err)
// body, err := rsp.MarshalJSON()
// require.NoError(t, err)
// //fmt.Printf("GOT: %v\n", string(body))
// require.JSONEq(t, `{
// "apiVersion": "testdata.datasource.grafana.app/v0alpha1",
// "code": 1,
// "kind": "HealthCheckResult",
// "message": "Data source is working",
// "status": "OK"
// }
// `, string(body))
secure, err = obj.GetSecureValues()
require.NoError(t, err)
keys = slices.Collect(maps.Keys(secure))
require.ElementsMatch(t, []string{"bbb", "ccc"}, keys)
// 3. LIST
list, err := client.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1, "expected a single datasource")
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec")
jj, _ := json.MarshalIndent(spec, "", " ")
// fmt.Printf("%s\n", string(jj))
require.JSONEq(t, `{
"access": "proxy",
"database": "testdb",
"isDefault": true,
"jsonData": {
"hello": "world"
},
"readOnly": true,
"title": "test",
"url": "http://fake.url",
"user": "example"
}`, string(jj))
// 4. Call functions that use the stored configs
t.Run("subresources", func(t *testing.T) {
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
ctx := context.Background()
list, err := client.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1, "expected a single connection")
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
_, err = client.Get(ctx, "test", metav1.GetOptions{}, "health")
// endpoint is disabled currently because it has not been
// sufficiently tested.
// for more info see pkg/registry/apis/datasource/sub_health.go
require.Error(t, err)
var statusErr *apierrors.StatusError
require.True(t, errors.As(err, &statusErr))
require.Equal(t, int32(501), statusErr.ErrStatus.Code)
// require.NoError(t, err)
// body, err := rsp.MarshalJSON()
// require.NoError(t, err)
// //fmt.Printf("GOT: %v\n", string(body))
// require.JSONEq(t, `{
// "apiVersion": "testdata.datasource.grafana.app/v0alpha1",
// "code": 1,
// "kind": "HealthCheckResult",
// "message": "Data source is working",
// "status": "OK"
// }
// `, string(body))
// Test connecting to non-JSON marshaled data
raw := apis.DoRequest[any](helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "GET",
Path: "/apis/testdata.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))
})
})
}
// Test connecting to non-JSON marshaled data
raw := apis.DoRequest[any](helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "GET",
Path: "/apis/testdata.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))
})
}