K8s: Dashboards /apis: Fix library element connections (#106734)

This commit is contained in:
Stephanie Hingtgen
2025-06-13 14:40:39 -05:00
committed by GitHub
parent a8886ad5ec
commit feeced9618
5 changed files with 110 additions and 3 deletions

View File

@ -65,7 +65,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
migrator := legacy.NewDashboardAccess( migrator := legacy.NewDashboardAccess(
legacysql.NewDatabaseProvider(sqlStore), legacysql.NewDatabaseProvider(sqlStore),
authlib.OrgNamespaceFormatter, authlib.OrgNamespaceFormatter,
nil, provisioning, sort.ProvideService(), nil, provisioning, nil, sort.ProvideService(),
) )
if c.Bool("non-interactive") { if c.Bool("non-interactive") {

View File

@ -15,6 +15,7 @@ import (
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/db" "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/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
@ -46,9 +47,10 @@ type LegacyMigrator interface {
func ProvideLegacyMigrator( func ProvideLegacyMigrator(
sql db.DB, // direct access to tables sql db.DB, // direct access to tables
provisioning provisioning.ProvisioningService, // only needed for dashboard settings provisioning provisioning.ProvisioningService, // only needed for dashboard settings
libraryPanelSvc librarypanels.Service,
) LegacyMigrator { ) LegacyMigrator {
dbp := legacysql.NewDatabaseProvider(sql) 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 { type BlobStoreInfo struct {

View File

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards" "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/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/legacysql"
@ -63,6 +64,8 @@ type dashboardSqlAccess struct {
dashStore dashboards.Store dashStore dashboards.Store
dashboardSearchClient legacysearcher.DashboardSearchClient dashboardSearchClient legacysearcher.DashboardSearchClient
libraryPanelSvc librarypanels.Service
// Typically one... the server wrapper // Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent subscribers []chan *resource.WrittenEvent
mutex sync.Mutex mutex sync.Mutex
@ -73,6 +76,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
namespacer request.NamespaceMapper, namespacer request.NamespaceMapper,
dashStore dashboards.Store, dashStore dashboards.Store,
provisioning provisioning.ProvisioningService, provisioning provisioning.ProvisioningService,
libraryPanelSvc librarypanels.Service,
sorter sort.Service, sorter sort.Service,
) DashboardAccess { ) DashboardAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
@ -82,6 +86,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
dashStore: dashStore, dashStore: dashStore,
provisioning: provisioning, provisioning: provisioning,
dashboardSearchClient: *dashboardSearchClient, dashboardSearchClient: *dashboardSearchClient,
libraryPanelSvc: libraryPanelSvc,
log: log.New("dashboard.legacysql"), 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") 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) // stash the raw value in context (if requested)
finalMeta, err := utils.MetaAccessor(dash) finalMeta, err := utils.MetaAccessor(dash)
if err != nil { if err != nil {

View File

@ -39,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder" "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/provisioning"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/search/sort"
@ -110,6 +111,7 @@ func RegisterAPIService(
sorter sort.Service, sorter sort.Service,
quotaService quota.Service, quotaService quota.Service,
folderStore folder.FolderStore, folderStore folder.FolderStore,
libraryPanelSvc librarypanels.Service,
restConfigProvider apiserver.RestConfigProvider, restConfigProvider apiserver.RestConfigProvider,
userService user.Service, userService user.Service,
) *DashboardsAPIBuilder { ) *DashboardsAPIBuilder {
@ -137,7 +139,7 @@ func RegisterAPIService(
folderClient: folderClient, folderClient: folderClient,
legacy: &DashboardStorage{ legacy: &DashboardStorage{
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, sorter), Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter),
DashboardService: dashboardService, DashboardService: dashboardService,
}, },
reg: reg, reg: reg,

View File

@ -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
}