fix: create dashboard in legacy storage within transaction (#114808)

fix: create dashboard within transaction
This commit is contained in:
Renato Costa
2025-12-09 10:16:33 -05:00
committed by GitHub
parent 8c4b3d1702
commit 83311049ad
3 changed files with 75 additions and 3 deletions
@@ -140,11 +140,30 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider,
}
func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query string, args ...any) (*sql.Rows, error) {
// Use transaction if available in context.
var tx *sql.Tx
// After this function runs, the `tx` variable will only be set if
// this function was called in the context of a transaction set up by a
// caller upstream. In that case, we reuse the transaction.
_ = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
coreTx, err := sess.Tx()
if err != nil {
return nil
}
tx = coreTx.Tx
return nil
})
// Use transaction from unified storage if available in the context.
// This allows us to run migrations in a transaction which is specifically required for SQLite.
if tx := resource.TransactionFromContext(ctx); tx != nil {
if tx == nil {
tx = resource.TransactionFromContext(ctx)
}
if tx != nil {
return tx.QueryContext(ctx, query, args...)
}
return helper.DB.GetSqlxSession().Query(ctx, query, args...)
}
+10 -1
View File
@@ -132,10 +132,19 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ
}
} else {
failOnExisting := event.Type == resourcepb.WatchEvent_ADDED
after, _, err := a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting)
sql, err := a.sql(ctx)
if err != nil {
return 0, err
}
var after *dashboard.Dashboard
if err := sql.DB.InTransaction(ctx, func(ctx context.Context) error {
var err error
after, _, err = a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting)
return err
}); err != nil {
return 0, err
}
if after != nil {
meta, err := utils.MetaAccessor(after)
if err != nil {
@@ -233,6 +233,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) {
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"dashboard": map[string]interface{}{
@@ -266,6 +267,34 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) {
originalCount := getDashboardCount(t, grafanaListedAddr, "admin", "admin")
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"dashboard": map[string]interface{}{
"title": "Bad dashboard",
"panels": []interface{}{
map[string]interface{}{
"gridPos": map[string]int{"h": 0, "w": 0, "x": 0, "y": 0},
"libraryPanel": map[string]string{
"name": "Bad panel",
"uid": "invalid-uid",
},
},
},
},
})
require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(body), "library element could not be found")
err = resp.Body.Close()
require.NoError(t, err)
// A new dashboard is not created in this situation.
require.Equal(t, originalCount, getDashboardCount(t, grafanaListedAddr, "admin", "admin"))
})
}
func TestIntegrationDashboardQuota(t *testing.T) {
@@ -982,6 +1011,21 @@ func postDashboard(t *testing.T, grafanaListedAddr, user, password string, paylo
return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
}
func getDashboardCount(t *testing.T, grafanaListenAddr, user, password string) int {
endpoint := fmt.Sprintf("http://%s:%s@%s/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search", user, password, grafanaListenAddr)
resp, err := http.Get(endpoint) //nolint:gosec
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
var payload map[string]any
require.NoError(t, json.Unmarshal(body, &payload))
return int(payload["totalHits"].(float64))
}
func TestIntegrationDashboardServicePermissions(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)