diff --git a/pkg/services/dashboards/service/client/client.go b/pkg/services/dashboards/service/client/client.go index 7a3404b50f8..dd9d306d97d 100644 --- a/pkg/services/dashboards/service/client/client.go +++ b/pkg/services/dashboards/service/client/client.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/apiserver" @@ -47,7 +47,7 @@ func NewK8sClientWithFallback( ) *K8sClientWithFallback { newClientFunc := newK8sClientFactory(cfg, restConfigProvider, dashboardStore, userService, resourceClient, sorter, dual) return &K8sClientWithFallback{ - K8sHandler: newClientFunc(context.Background(), dashboardv1.VERSION), + K8sHandler: newClientFunc(context.Background(), dashboardv0.VERSION), newClientFunc: newClientFunc, metrics: newK8sClientMetrics(reg), log: log.New("dashboards-k8s-client"), @@ -64,7 +64,7 @@ func (h *K8sClientWithFallback) Get(ctx context.Context, name string, orgID int6 attribute.Bool("fallback", false), ) - span.AddEvent("v1alpha1 Get") + span.AddEvent("v0alpha1 Get") result, err := h.K8sHandler.Get(spanCtx, name, orgID, options, subresources...) if err != nil { return nil, tracing.Error(span, err) @@ -117,7 +117,7 @@ func newK8sClientFactory( cacheMutex := &sync.RWMutex{} return func(ctx context.Context, version string) client.K8sHandler { _, span := tracing.Start(ctx, "k8sClientFactory.GetClient", - attribute.String("group", dashboardv1.GROUP), + attribute.String("group", dashboardv0.GROUP), attribute.String("version", version), attribute.String("resource", "dashboards"), ) @@ -143,7 +143,7 @@ func newK8sClientFactory( } gvr := schema.GroupVersionResource{ - Group: dashboardv1.GROUP, + Group: dashboardv0.GROUP, Version: version, Resource: "dashboards", } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index bd7e3003d93..9466d45ea0d 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -27,7 +27,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -2069,13 +2068,13 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex switch query.Type { case "": // When no type specified, search for dashboards - request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1.DASHBOARD_RESOURCE) + request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv0.DASHBOARD_RESOURCE) // Currently a search query is across folders and dashboards if err == nil { federate, err = resource.AsResourceKey(namespace, folderv1.RESOURCE) } case searchstore.TypeDashboard, searchstore.TypeAnnotation: - request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1.DASHBOARD_RESOURCE) + request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv0.DASHBOARD_RESOURCE) case searchstore.TypeFolder, searchstore.TypeAlertFolder: request.Options.Key, err = resource.AsResourceKey(namespace, folderv1.RESOURCE) default: @@ -2262,7 +2261,7 @@ func (dr *DashboardServiceImpl) unstructuredToLegacyDashboardWithUsers(item *uns FolderUID: obj.GetFolder(), Version: int(dashVersion), Data: simplejson.NewFromAny(spec), - APIVersion: strings.TrimPrefix(item.GetAPIVersion(), dashboardv1.GROUP+"/"), + APIVersion: strings.TrimPrefix(item.GetAPIVersion(), dashboardv0.GROUP+"/"), } out.Created = obj.GetCreationTimestamp().Time @@ -2351,7 +2350,7 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names finalObj.Object["spec"] = obj finalObj.SetName(uid) finalObj.SetNamespace(namespace) - finalObj.SetGroupVersionKind(dashboardv1.DashboardResourceInfo.GroupVersionKind()) + finalObj.SetGroupVersionKind(dashboardv0.DashboardResourceInfo.GroupVersionKind()) meta, err := utils.MetaAccessor(finalObj) if err != nil { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 6870f0bae3b..bf2b3a0f5e2 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -16,7 +16,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apiserver/pkg/endpoints/request" - dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" @@ -543,8 +543,8 @@ func TestGetProvisionedDashboardData(t *testing.T) { k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -649,8 +649,8 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { provisioningTimestamp := int64(1234567) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -743,8 +743,8 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { provisioningTimestamp := int64(1234567) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -976,8 +976,8 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -1121,7 +1121,7 @@ func TestUnprovisionDashboard(t *testing.T) { }} k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": dashboardv1.APIVERSION, + "apiVersion": dashboardv0.APIVERSION, "kind": "Dashboard", "metadata": map[string]any{ "name": "uid", @@ -2502,7 +2502,7 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) { // Create test object key := &resource.ResourceKey{Group: "dashboard.grafana.app", Resource: "dashboards", Name: "test", Namespace: "default"} - obj := &dashboardv1.Dashboard{ + obj := &dashboardv0.Dashboard{ TypeMeta: metav1.TypeMeta{ APIVersion: "dashboard.grafana.app/v0alpha1", }, @@ -2857,8 +2857,8 @@ func TestK8sDashboardCleanupJob(t *testing.T) { func createTestUnstructuredDashboard(uid, title string, resourceVersion string) unstructured.Unstructured { return unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": uid, "deletionTimestamp": "2023-01-01T00:00:00Z", diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 49be4af1883..11c440b3352 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -11,7 +11,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" @@ -55,7 +55,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash k8sclient: client.NewK8sHandler( dual, request.GetNamespaceMapper(cfg), - dashv1.DashboardResourceInfo.GroupVersionResource(), + dashv0.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index d761502cb2d..9c59dda732f 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -340,6 +340,14 @@ func TestIntegrationCreateK8s(t *testing.T) { testCreate(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) } +func TestIntegrationPreserveSchemaVersion(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + testPreserveSchemaVersion(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) +} + func testCreate(t *testing.T, featureToggles []string) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ @@ -499,3 +507,105 @@ func createFolder(t *testing.T, grafanaListedAddr string, title string) *dtos.Fo return f } + +func intPtr(n int) *int { + return &n +} + +func testPreserveSchemaVersion(t *testing.T, featureToggles []string) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: featureToggles, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + store, cfg := env.SQLStore, env.Cfg + + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + }) + + schemaVersions := []*int{intPtr(1), intPtr(36), intPtr(40), nil} + for _, schemaVersion := range schemaVersions { + var title string + if schemaVersion == nil { + title = "save dashboard with no schemaVersion" + } else { + title = fmt.Sprintf("save dashboard with schemaVersion %d", *schemaVersion) + } + + t.Run(title, func(t *testing.T) { + // Create dashboard JSON with specified schema version + var dashboardJSON string + if schemaVersion != nil { + dashboardJSON = fmt.Sprintf(`{"title":"Schema Version Test", "schemaVersion": %d}`, *schemaVersion) + } else { + dashboardJSON = `{"title":"Schema Version Test"}` + } + + dashboardData, err := simplejson.NewJson([]byte(dashboardJSON)) + require.NoError(t, err) + + // Save the dashboard via API + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(dashboards.SaveDashboardCommand{ + Dashboard: dashboardData, + }) + require.NoError(t, err) + + url := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(url, "application/json", buf) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + + // Get dashboard UID from response + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var saveResp struct { + UID string `json:"uid"` + } + err = json.Unmarshal(b, &saveResp) + require.NoError(t, err) + require.NotEmpty(t, saveResp.UID) + + getDashURL := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s", grafanaListedAddr, saveResp.UID) + // nolint:gosec + getResp, err := http.Get(getDashURL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, getResp.StatusCode) + t.Cleanup(func() { + err := getResp.Body.Close() + require.NoError(t, err) + }) + + // Parse response and check if schema version is preserved + dashBody, err := io.ReadAll(getResp.Body) + require.NoError(t, err) + + var dashResp struct { + Dashboard *simplejson.Json `json:"dashboard"` + } + err = json.Unmarshal(dashBody, &dashResp) + require.NoError(t, err) + + actualSchemaVersion := dashResp.Dashboard.Get("schemaVersion") + if schemaVersion != nil { + // Check if schemaVersion is preserved (not migrated to latest) + actualVersion := actualSchemaVersion.MustInt() + require.Equal(t, *schemaVersion, actualVersion, + "Dashboard schemaVersion should not be automatically changed when saved through /api/dashboards/db") + } else { + actualVersion, err := actualSchemaVersion.Int() + s, _ := dashResp.Dashboard.EncodePretty() + require.Error(t, err, fmt.Sprintf("Dashboard schemaVersion should not be automatically populated when saved through /api/dashboards/db, was %d. %s", actualVersion, string(s))) + } + }) + } +} diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index a630d12bf34..3e4c75f11cd 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -350,7 +350,7 @@ func TestIntegrationLegacySupport(t *testing.T) { Path: "/api/dashboards/uid/test-v1", }, &dtos.DashboardFullWithMeta{}) require.Equal(t, 200, rsp.Response.StatusCode) - require.Equal(t, "v1alpha1", rsp.Result.Meta.APIVersion) + require.Equal(t, "v0alpha1", rsp.Result.Meta.APIVersion) // v0alpha1 is used as the default version for /api // V2 should send a not acceptable rsp = apis.DoRequest(helper, apis.RequestParams{