mirror of
https://github.com/grafana/grafana.git
synced 2026-08-13 14:45:02 -05:00
Dashboards: Improve support for writing k8s dashboards to the legacy API (#119538)
This commit is contained in:
+58
-5
@@ -413,9 +413,10 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
|
||||
return response.Error(http.StatusBadRequest, "Failed to read dashboard", err)
|
||||
}
|
||||
|
||||
// Items with v2 schema elements must set v2 properties
|
||||
// Check for v2 schema elements without a k8s style wrapper
|
||||
if dashboards.LooksLikeV2Spec(spec) {
|
||||
return response.Error(http.StatusBadRequest, dashboards.LooksLikeV2SpecMessage, nil)
|
||||
return response.Error(http.StatusBadRequest, dashboards.LooksLikeV2SpecMessage+
|
||||
" OR it should include a object wrapper with an explicit 'apiVersion' and move the body into a 'spec' element", nil)
|
||||
}
|
||||
|
||||
// Items with metadata, spec, etc
|
||||
@@ -543,12 +544,48 @@ func (hs *HTTPServer) saveDashboardViaK8s(c *contextmodel.ReqContext, cmd dashbo
|
||||
meta.SetManagedFields(nil)
|
||||
|
||||
name := obj.GetName()
|
||||
if name == "" {
|
||||
name, _, _ = unstructured.NestedString(obj.Object, "spec", "uid")
|
||||
}
|
||||
|
||||
// Check (and remove) any legacy internal IDs
|
||||
var old *unstructured.Unstructured
|
||||
internalID, err := nestedInternalID(obj.Object)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, err.Error(), err)
|
||||
}
|
||||
if internalID > 0 && name == "" {
|
||||
found, err := client.List(ctx, metav1.ListOptions{
|
||||
LabelSelector: fmt.Sprintf("%s=%d", utils.LabelKeyDeprecatedInternalID, internalID),
|
||||
Limit: 2,
|
||||
})
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "unable to lookup previous version", err)
|
||||
}
|
||||
if len(found.Items) == 0 {
|
||||
return response.Error(http.StatusBadRequest,
|
||||
fmt.Sprintf("The payload includes an internal identifier (%d) that is not found", internalID), nil)
|
||||
}
|
||||
|
||||
old = &found.Items[0]
|
||||
name = old.GetName()
|
||||
meta.SetName(name)
|
||||
if !cmd.Overwrite {
|
||||
return response.Error(http.StatusConflict,
|
||||
"Dashboard with the same internal ID already exists. Use overwrite flag to update.", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Never send internal ID or UID in the body
|
||||
unstructured.RemoveNestedField(obj.Object, "spec", "id")
|
||||
unstructured.RemoveNestedField(obj.Object, "spec", "uid")
|
||||
|
||||
isCreate := name == ""
|
||||
if isCreate {
|
||||
obj.SetGenerateName("a") // prefix
|
||||
} else {
|
||||
} else if old == nil {
|
||||
// Read the old value first
|
||||
old, err := client.Get(ctx, name, metav1.GetOptions{})
|
||||
old, err = client.Get(ctx, name, metav1.GetOptions{})
|
||||
if err == nil && old != nil {
|
||||
if !cmd.Overwrite {
|
||||
return response.Error(http.StatusConflict,
|
||||
@@ -582,7 +619,7 @@ func (hs *HTTPServer) saveDashboardViaK8s(c *contextmodel.ReqContext, cmd dashbo
|
||||
|
||||
meta, err = utils.MetaAccessor(dash)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to save dashboard", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed get meta accessor", err)
|
||||
}
|
||||
|
||||
title, _, _ = unstructured.NestedString(dash.Object, "spec", "title")
|
||||
@@ -599,6 +636,22 @@ func (hs *HTTPServer) saveDashboardViaK8s(c *contextmodel.ReqContext, cmd dashbo
|
||||
})
|
||||
}
|
||||
|
||||
func nestedInternalID(obj map[string]interface{}) (int64, error) {
|
||||
val, found, err := unstructured.NestedFieldNoCopy(obj, "spec", "id")
|
||||
if !found || err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
i, ok := val.(int64)
|
||||
if ok {
|
||||
return i, nil
|
||||
}
|
||||
n, ok := val.(json.Number)
|
||||
if ok {
|
||||
return n.Int64()
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported ID type: %T", val)
|
||||
}
|
||||
|
||||
// swagger:route GET /dashboards/home dashboards getHomeDashboard
|
||||
//
|
||||
// NOTE: the home dashboard is configured in preferences. This API will be removed in G13
|
||||
|
||||
@@ -77,6 +77,8 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }}
|
||||
{{ else }}
|
||||
{{ if .Query.UID }}
|
||||
AND dashboard.uid = {{ .Arg .Query.UID }}
|
||||
{{ else if .Query.DeprecatedInternalID }}
|
||||
AND dashboard.id = {{ .Arg .Query.DeprecatedInternalID }}
|
||||
{{ else if .Query.LastID }}
|
||||
AND dashboard.id < {{ .Arg .Query.LastID }}
|
||||
{{ end }}
|
||||
|
||||
@@ -154,12 +154,30 @@ func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql
|
||||
return helper.DB.GetSqlxSession().Query(ctx, query, args...)
|
||||
}
|
||||
|
||||
func getLegacyIDSelector(labels []*resourcepb.Requirement) int64 {
|
||||
if len(labels) != 1 {
|
||||
return 0
|
||||
}
|
||||
q := labels[0]
|
||||
if q.Key == utils.LabelKeyDeprecatedInternalID &&
|
||||
q.Operator == "=" &&
|
||||
len(q.Values) == 1 {
|
||||
i, _ := strconv.ParseInt(labels[0].Values[0], 10, 64)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) getRows(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query *DashboardQuery) (*rowsWrapper, error) {
|
||||
ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.getRows")
|
||||
defer span.End()
|
||||
|
||||
if len(query.Labels) > 0 {
|
||||
return nil, fmt.Errorf("labels not yet supported")
|
||||
query.DeprecatedInternalID = getLegacyIDSelector(query.Labels)
|
||||
if query.DeprecatedInternalID == 0 {
|
||||
return nil, fmt.Errorf("labels not yet supported")
|
||||
}
|
||||
|
||||
// if query.Requirements.Folder != nil {
|
||||
// args = append(args, *query.Requirements.Folder)
|
||||
// sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=?$%d", sqlcmd, len(args))
|
||||
|
||||
@@ -17,6 +17,8 @@ type DashboardQuery struct {
|
||||
UID string // to select a single dashboard
|
||||
Limit int
|
||||
|
||||
DeprecatedInternalID int64 // to select a single dashboard
|
||||
|
||||
// MaxRows is used internally by the iterator to fetch data in batches
|
||||
// When set, the SQL query will include LIMIT MaxRows
|
||||
// If Limit is smaller, that will be used instead
|
||||
|
||||
@@ -300,12 +300,25 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validate k8s payload in legacy API", func(t *testing.T) {
|
||||
t.Run("use k8s payload in legacy API", func(t *testing.T) {
|
||||
cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig())
|
||||
cfg.GroupVersion = &dashboardV0.GroupVersion
|
||||
adminClient, err := k8srest.RESTClientFor(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Construct a legacy api payload from a k8s object
|
||||
getLegacySaveCommand := func(obj *unstructured.Unstructured, title string, overwrite bool) []byte {
|
||||
err := unstructured.SetNestedField(obj.Object, title, "spec", "title") // update the title
|
||||
require.NoError(t, err)
|
||||
cmd := map[string]any{
|
||||
"dashboard": obj.Object,
|
||||
"overwrite": overwrite,
|
||||
}
|
||||
jj, err := json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
return jj
|
||||
}
|
||||
|
||||
names := []string{"test-v0", "test-v1", "test-v2"}
|
||||
clients := []dynamic.ResourceInterface{
|
||||
clientV0.Resource,
|
||||
@@ -339,14 +352,13 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
foundTitle, _, _ := unstructured.NestedString(found.Object, "spec", "title")
|
||||
require.Equal(t, title, foundTitle, "in object: %s", obj.GetName())
|
||||
|
||||
// ID must not be a saved element
|
||||
_, ok, _ := unstructured.NestedInt64(obj.Object, "spec", "id")
|
||||
require.False(t, ok, "internal id should not be part of the saved spec")
|
||||
|
||||
// Update the title -- try to save without overwrite=false
|
||||
title = "update:" + name
|
||||
err = unstructured.SetNestedField(obj.Object, title, "spec", "title") // update the title
|
||||
require.NoError(t, err)
|
||||
jj, err = obj.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
body = []byte(`{"dashboard": ` + string(jj) + `, "overwrite": false}`)
|
||||
body = getLegacySaveCommand(obj, title, false)
|
||||
_ = adminClient.Post().AbsPath("api", "dashboards", "db").
|
||||
Body(body).
|
||||
SetHeader("Content-type", "application/json").
|
||||
@@ -355,7 +367,7 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
require.Equal(t, int(http.StatusConflict), statusCode) // already exists
|
||||
|
||||
// Overwrite!
|
||||
body = []byte(`{"dashboard": ` + string(jj) + `, "overwrite": true}`)
|
||||
body = getLegacySaveCommand(obj, title, true)
|
||||
_ = adminClient.Post().AbsPath("api", "dashboards", "db").
|
||||
Body(body).
|
||||
SetHeader("Content-type", "application/json").
|
||||
@@ -378,6 +390,30 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
err = json.Unmarshal(jj, dto)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, dto.Dashboard.Get("title").MustString(""), "in object: %s", obj.GetName())
|
||||
|
||||
// Update by internal id (without name)
|
||||
meta, err := utils.MetaAccessor(found)
|
||||
require.NoError(t, err)
|
||||
internalId := meta.GetDeprecatedInternalID() // nolint:staticcheck
|
||||
require.True(t, internalId > 0)
|
||||
|
||||
title = "updated using internal ID"
|
||||
unstructured.RemoveNestedField(obj.Object, "spec", "uid")
|
||||
unstructured.RemoveNestedField(obj.Object, "metadata", "name")
|
||||
err = unstructured.SetNestedField(obj.Object, internalId, "spec", "id")
|
||||
require.NoError(t, err)
|
||||
body = getLegacySaveCommand(obj, title, true)
|
||||
rsp := adminClient.Post().AbsPath("api", "dashboards", "db").
|
||||
Body(body).
|
||||
SetHeader("Content-type", "application/json").
|
||||
Do(ctx).
|
||||
StatusCode(&statusCode)
|
||||
require.Equal(t, int(http.StatusOK), statusCode) // already exists
|
||||
body, _ = rsp.Raw()
|
||||
err = json.Unmarshal(body, &obj.Object)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, name+"-legacy", obj.Object["uid"])
|
||||
require.Equal(t, float64(internalId), obj.Object["id"]) // same internal ID
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -174,6 +174,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
dashboard.timezone = timeRange.timeZone;
|
||||
}
|
||||
|
||||
delete dashboard.id; // Make sure we never save an internal ID
|
||||
return sortedDeepCloneWithoutNulls(dashboard, true);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user