From feeced9618ee6c522e5b9a38298fff14a62aa407 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 13 Jun 2025 14:40:39 -0500 Subject: [PATCH] K8s: Dashboards /apis: Fix library element connections (#106734) --- .../datamigrations/to_unified_storage.go | 2 +- pkg/registry/apis/dashboard/legacy/migrate.go | 4 +- .../apis/dashboard/legacy/sql_dashboards.go | 16 ++++ pkg/registry/apis/dashboard/register.go | 4 +- .../integration/api_validation_test.go | 87 +++++++++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index 79901c746d2..ad3d5979d74 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -65,7 +65,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err migrator := legacy.NewDashboardAccess( legacysql.NewDatabaseProvider(sqlStore), authlib.OrgNamespaceFormatter, - nil, provisioning, sort.ProvideService(), + nil, provisioning, nil, sort.ProvideService(), ) if c.Bool("non-interactive") { diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index 7c08bb14c8b..6fd0225f45e 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -15,6 +15,7 @@ import ( folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -46,9 +47,10 @@ type LegacyMigrator interface { func ProvideLegacyMigrator( sql db.DB, // direct access to tables provisioning provisioning.ProvisioningService, // only needed for dashboard settings + libraryPanelSvc librarypanels.Service, ) LegacyMigrator { dbp := legacysql.NewDatabaseProvider(sql) - return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, sort.ProvideService()) + return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService()) } type BlobStoreInfo struct { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 555395fbee4..6f04fe12947 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/storage/legacysql" @@ -63,6 +64,8 @@ type dashboardSqlAccess struct { dashStore dashboards.Store dashboardSearchClient legacysearcher.DashboardSearchClient + libraryPanelSvc librarypanels.Service + // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent mutex sync.Mutex @@ -73,6 +76,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService, + libraryPanelSvc librarypanels.Service, sorter sort.Service, ) DashboardAccess { dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) @@ -82,6 +86,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashStore: dashStore, provisioning: provisioning, dashboardSearchClient: *dashboardSearchClient, + libraryPanelSvc: libraryPanelSvc, log: log.New("dashboard.legacysql"), } } @@ -450,6 +455,17 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das return nil, false, fmt.Errorf("unable to retrieve dashboard after save") } + // TODO: for modes 3+, we need to migrate /api to /apis for library connections, and begin to + // use search to return the connections, rather than the connections table. + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, false, err + } + err = a.libraryPanelSvc.ConnectLibraryPanelsForDashboard(ctx, requester, out) + if err != nil { + return nil, false, err + } + // stash the raw value in context (if requested) finalMeta, err := utils.MetaAccessor(dash) if err != nil { diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 92c09689838..eaa7d40767e 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -39,6 +39,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/sort" @@ -110,6 +111,7 @@ func RegisterAPIService( sorter sort.Service, quotaService quota.Service, folderStore folder.FolderStore, + libraryPanelSvc librarypanels.Service, restConfigProvider apiserver.RestConfigProvider, userService user.Service, ) *DashboardsAPIBuilder { @@ -137,7 +139,7 @@ func RegisterAPIService( folderClient: folderClient, legacy: &DashboardStorage{ - Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, sorter), + Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter), DashboardService: dashboardService, }, reg: reg, diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index a7422606b4d..1b92c7cd182 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -2400,3 +2400,90 @@ func runDashboardListTest(t *testing.T, ctx TestContext) { } }) } + +// TODO: this only works on mode0-3 right now. In modes 4/5, we need to start returning the connections endpoint +// from retrieving the panel count from search / indexing the dashboard library panels +func TestDashboardWithLibraryPanel(t *testing.T) { + dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3} + for _, dualWriterMode := range dualWriterModes { + t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{ + "unifiedStorageSearch", + "kubernetesClientDashboardsFolders", + }, + }) + ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()) + + // create the library element first + libraryElement := map[string]interface{}{ + "kind": 1, + "name": "Test Library Panel", + "model": map[string]interface{}{ + "type": "timeseries", + "title": "Test Library Panel", + }, + } + libraryElementURL := "/api/library-elements" + libraryElementData, err := postHelper(t, &ctx, libraryElementURL, libraryElement, ctx.AdminUser) + require.NoError(t, err) + require.NotNil(t, libraryElementData) + data := libraryElementData["result"].(map[string]interface{}) + uid := data["uid"].(string) + require.NotEmpty(t, uid) + + // then reference the library element in the dashboard + dashboard := createDashboardObject(t, "Library Panel Test", "", 1) + dashboard.Object["spec"].(map[string]interface{})["panels"] = []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Library Panel", + "type": "library-panel-ref", + "libraryPanel": map[string]interface{}{ + "uid": uid, + "name": "Test Library Panel", + }, + }, + } + + createdDash, err := adminClient.Resource.Create(context.Background(), dashboard, v1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, createdDash) + + // should have created a library panel connection + connectionsURL := fmt.Sprintf("/api/library-elements/%s/connections", uid) + connectionsData, err := getDashboardViaHTTP(t, &ctx, connectionsURL, ctx.AdminUser) + require.NoError(t, err) + require.NotNil(t, connectionsData) + connections := connectionsData["result"].([]interface{}) + require.Len(t, connections, 1) + }) + } +} + +func postHelper(t *testing.T, ctx *TestContext, path string, body interface{}, user apis.User) (map[string]interface{}, error) { + bodyJSON, err := json.Marshal(body) + require.NoError(t, err) + + resp := apis.DoRequest(ctx.Helper, apis.RequestParams{ + User: user, + Method: http.MethodPost, + Path: path, + Body: bodyJSON, + ContentType: "application/json", + }, &struct{}{}) + + if resp.Response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to post: %s", resp.Response.Status) + } + + var result map[string]interface{} + err = json.Unmarshal(resp.Body, &result) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response JSON: %v", err) + } + + return result, nil +}