mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 03:41:50 +08:00
K8s: Dashboards /apis: Fix library element connections (#106734)
This commit is contained in:

committed by
GitHub

parent
a8886ad5ec
commit
feeced9618
@ -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") {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user