mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Fix legacy fallback provisioning (#100566)
This commit is contained in:
parent
3b694785f3
commit
e2081c3e0c
51
pkg/apis/dashboard/utils.go
Normal file
51
pkg/apis/dashboard/utils.go
Normal file
@ -0,0 +1,51 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
var PluginIDRepoName = "plugin"
|
||||
var fileProvisionedRepoPrefix = "file:"
|
||||
|
||||
// ProvisionedFileNameWithPrefix adds the `file:` prefix to the
|
||||
// provisioner name, to be used as the annotation for dashboards
|
||||
// provisioned from files
|
||||
func ProvisionedFileNameWithPrefix(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fileProvisionedRepoPrefix + name
|
||||
}
|
||||
|
||||
// GetProvisionedFileNameFromMeta returns the provisioner name
|
||||
// from a given annotation string, which is in the form file:<name>
|
||||
func GetProvisionedFileNameFromMeta(annotation string) (string, bool) {
|
||||
return strings.CutPrefix(annotation, fileProvisionedRepoPrefix)
|
||||
}
|
||||
|
||||
// SetPluginIDMeta sets the repo name to "plugin" and the path to the plugin ID
|
||||
func SetPluginIDMeta(obj unstructured.Unstructured, pluginID string) {
|
||||
if pluginID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[utils.AnnoKeyRepoName] = PluginIDRepoName
|
||||
annotations[utils.AnnoKeyRepoPath] = pluginID
|
||||
obj.SetAnnotations(annotations)
|
||||
}
|
||||
|
||||
// GetPluginIDFromMeta returns the plugin ID from the meta if the repo name is "plugin"
|
||||
func GetPluginIDFromMeta(obj utils.GrafanaMetaAccessor) string {
|
||||
if obj.GetRepositoryName() == PluginIDRepoName {
|
||||
return obj.GetRepositoryPath()
|
||||
}
|
||||
return ""
|
||||
}
|
@ -23,7 +23,6 @@ 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/dashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@ -312,7 +311,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
||||
ts := time.Unix(origin_ts.Int64, 0)
|
||||
|
||||
repo := &utils.ResourceRepositoryInfo{
|
||||
Name: origin_name.String,
|
||||
Name: dashboard.ProvisionedFileNameWithPrefix(origin_name.String),
|
||||
Hash: origin_hash.String,
|
||||
Timestamp: &ts,
|
||||
}
|
||||
@ -331,7 +330,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
|
||||
meta.SetRepositoryInfo(repo)
|
||||
} else if plugin_id.String != "" {
|
||||
meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
|
||||
Name: "plugin",
|
||||
Name: dashboard.PluginIDRepoName,
|
||||
Path: plugin_id.String,
|
||||
})
|
||||
}
|
||||
@ -427,7 +426,7 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
|
||||
out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{
|
||||
OrgID: orgId,
|
||||
Message: meta.GetMessage(),
|
||||
PluginID: service.GetPluginIDFromMeta(meta),
|
||||
PluginID: dashboard.GetPluginIDFromMeta(meta),
|
||||
Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()),
|
||||
FolderUID: meta.GetFolder(),
|
||||
Overwrite: true, // already passed the revisionVersion checks!
|
||||
|
110
pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go
Normal file
110
pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestScanRow(t *testing.T) {
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer mockDB.Close() // nolint:errcheck
|
||||
|
||||
pathToFile := "path/to/file"
|
||||
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
|
||||
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
|
||||
store := &dashboardSqlAccess{
|
||||
namespacer: func(_ int64) string { return "default" },
|
||||
provisioning: provisioner,
|
||||
}
|
||||
|
||||
columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data"}
|
||||
id := int64(100)
|
||||
title := "Test Dashboard"
|
||||
folderUID := "folder123"
|
||||
timestamp := time.Now()
|
||||
k8sTimestamp := v1.Time{Time: timestamp}
|
||||
version := int64(2)
|
||||
message := "updated message"
|
||||
createdUser := "creator"
|
||||
updatedUser := "updator"
|
||||
|
||||
t.Run("Should scan a valid row correctly", func(t *testing.T) {
|
||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
|
||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||
resultRows, err := mockDB.Query("SELECT *")
|
||||
require.NoError(t, err)
|
||||
defer resultRows.Close() // nolint:errcheck
|
||||
resultRows.Next()
|
||||
|
||||
row, err := store.scanRow(resultRows, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, row)
|
||||
require.Equal(t, "Test Dashboard", row.Dash.Name)
|
||||
require.Equal(t, version, row.RV) // rv should be the dashboard version
|
||||
require.Equal(t, v0alpha1.Unstructured{
|
||||
Object: map[string]interface{}{"key": "value"},
|
||||
}, row.Dash.Spec)
|
||||
require.Equal(t, "default", row.Dash.Namespace)
|
||||
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
|
||||
|
||||
meta, err := utils.MetaAccessor(row.Dash)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, id, meta.GetDeprecatedInternalID()) // nolint:staticcheck
|
||||
require.Equal(t, version, meta.GetGeneration()) // generation should be dash version
|
||||
require.Equal(t, k8sTimestamp, meta.GetCreationTimestamp())
|
||||
require.Equal(t, "user:"+createdUser, meta.GetCreatedBy()) // should be prefixed by user:
|
||||
require.Equal(t, "user:"+updatedUser, meta.GetUpdatedBy()) // should be prefixed by user:
|
||||
require.Equal(t, message, meta.GetMessage())
|
||||
require.Equal(t, folderUID, meta.GetFolder())
|
||||
})
|
||||
|
||||
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
|
||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
|
||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||
resultRows, err := mockDB.Query("SELECT *")
|
||||
require.NoError(t, err)
|
||||
defer resultRows.Close() // nolint:errcheck
|
||||
resultRows.Next()
|
||||
|
||||
row, err := store.scanRow(resultRows, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, row)
|
||||
|
||||
meta, err := utils.MetaAccessor(row.Dash)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file:provisioner", meta.GetRepositoryName()) // should be prefixed by file:
|
||||
require.Equal(t, "../"+pathToFile, meta.GetRepositoryPath()) // relative to provisioner
|
||||
require.Equal(t, "hashing", meta.GetRepositoryHash())
|
||||
ts, err := meta.GetRepositoryTimestamp()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(100000), ts.Unix())
|
||||
})
|
||||
|
||||
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
|
||||
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
|
||||
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
||||
resultRows, err := mockDB.Query("SELECT *")
|
||||
require.NoError(t, err)
|
||||
defer resultRows.Close() // nolint:errcheck
|
||||
resultRows.Next()
|
||||
|
||||
row, err := store.scanRow(resultRows, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, row)
|
||||
|
||||
meta, err := utils.MetaAccessor(row.Dash)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "plugin", meta.GetRepositoryName())
|
||||
require.Equal(t, "slo", meta.GetRepositoryPath()) // the ID of the plugin
|
||||
require.Equal(t, "", meta.GetRepositoryHash()) // hash is not used on plugins
|
||||
})
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
)
|
||||
|
||||
type DashboardSearchClient struct {
|
||||
@ -29,6 +30,7 @@ func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchC
|
||||
return &DashboardSearchClient{dashboardStore: dashboardStore}
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
@ -103,6 +105,8 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
}
|
||||
|
||||
for _, field := range req.Options.Fields {
|
||||
vals := field.GetValues()
|
||||
|
||||
switch field.Key {
|
||||
case resource.SEARCH_FIELD_TAGS:
|
||||
query.Tags = field.GetValues()
|
||||
@ -110,7 +114,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
query.DashboardUIDs = field.GetValues()
|
||||
query.DashboardIds = nil
|
||||
case resource.SEARCH_FIELD_FOLDER:
|
||||
vals := field.GetValues()
|
||||
folders := make([]string, len(vals))
|
||||
|
||||
for i, val := range vals {
|
||||
@ -122,12 +125,28 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
}
|
||||
|
||||
query.FolderUIDs = folders
|
||||
}
|
||||
}
|
||||
case resource.SEARCH_FIELD_REPOSITORY_PATH:
|
||||
// only one value is supported in legacy search
|
||||
if len(vals) != 1 {
|
||||
return nil, fmt.Errorf("only one repo path query is supported")
|
||||
}
|
||||
query.ProvisionedPath = vals[0]
|
||||
case resource.SEARCH_FIELD_REPOSITORY_NAME:
|
||||
if field.Operator == string(selection.NotIn) {
|
||||
for _, val := range vals {
|
||||
name, _ := dashboard.GetProvisionedFileNameFromMeta(val)
|
||||
query.ProvisionedReposNotIn = append(query.ProvisionedReposNotIn, name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
res, err := c.dashboardStore.FindDashboards(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// only one value is supported in legacy search
|
||||
if len(vals) != 1 {
|
||||
return nil, fmt.Errorf("only one repo name is supported")
|
||||
}
|
||||
|
||||
query.ProvisionedRepo, _ = dashboard.GetProvisionedFileNameFromMeta(vals[0])
|
||||
}
|
||||
}
|
||||
|
||||
searchFields := resource.StandardSearchFields()
|
||||
@ -141,6 +160,41 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
},
|
||||
}
|
||||
|
||||
// if we are querying for provisioning information, we need to use a different
|
||||
// legacy sql query, since legacy search does not support this
|
||||
if query.ProvisionedRepo != "" || len(query.ProvisionedReposNotIn) > 0 {
|
||||
var dashes []*dashboards.Dashboard
|
||||
if query.ProvisionedRepo == dashboard.PluginIDRepoName {
|
||||
dashes, err = c.dashboardStore.GetDashboardsByPluginID(ctx, &dashboards.GetDashboardsByPluginIDQuery{
|
||||
PluginID: query.ProvisionedPath,
|
||||
OrgID: user.GetOrgID(),
|
||||
})
|
||||
} else if query.ProvisionedRepo != "" {
|
||||
dashes, err = c.dashboardStore.GetProvisionedDashboardsByName(ctx, query.ProvisionedRepo)
|
||||
} else if len(query.ProvisionedReposNotIn) > 0 {
|
||||
dashes, err = c.dashboardStore.GetOrphanedProvisionedDashboards(ctx, query.ProvisionedReposNotIn)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, dashboard := range dashes {
|
||||
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
|
||||
Key: getResourceKey(&dashboards.DashboardSearchProjection{
|
||||
UID: dashboard.UID,
|
||||
}, req.Options.Key.Namespace),
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), []byte{}},
|
||||
})
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
res, err := c.dashboardStore.FindDashboards(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hits := formatQueryResult(res)
|
||||
|
||||
for _, dashboard := range hits {
|
||||
|
263
pkg/registry/apis/dashboard/legacysearcher/search_client_test.go
Normal file
263
pkg/registry/apis/dashboard/legacysearcher/search_client_test.go
Normal file
@ -0,0 +1,263 @@
|
||||
package legacysearcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
)
|
||||
|
||||
func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
mockStore := dashboards.NewFakeDashboardStore(t)
|
||||
client := NewDashboardSearchClient(mockStore)
|
||||
ctx := context.Background()
|
||||
user := &user.SignedInUser{OrgID: 2}
|
||||
ctx = identity.WithRequester(ctx, user)
|
||||
|
||||
dashboardKey := &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
}
|
||||
|
||||
t.Run("Should parse results into GRPC", func(t *testing.T) {
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
|
||||
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags, err := json.Marshal([]string{"term"})
|
||||
require.NoError(t, err)
|
||||
|
||||
emptyTags, err := json.Marshal([]string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resp)
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder1"),
|
||||
tags,
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid2",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard2"),
|
||||
[]byte("folder2"),
|
||||
emptyTags,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Query should be set as the title, and * should be removed", func(t *testing.T) {
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
Title: "test",
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
Query: "*test*",
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Should read labels for the dashboard ids", func(t *testing.T) {
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int64{1, 2},
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Labels: []*resource.Requirement{
|
||||
{
|
||||
Key: utils.LabelKeyDeprecatedInternalID,
|
||||
Operator: "in",
|
||||
Values: []string{"1", "2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Should modify fields to legacy compatible queries", func(t *testing.T) {
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
DashboardUIDs: []string{"uid1", "uid2"},
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
FolderUIDs: []string{"general", "folder1"},
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_TAGS,
|
||||
Operator: "in",
|
||||
Values: []string{"tag1", "tag2"},
|
||||
},
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_NAME, // name should be used as uid
|
||||
Operator: "in",
|
||||
Values: []string{"uid1", "uid2"},
|
||||
},
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_FOLDER,
|
||||
Operator: "in",
|
||||
Values: []string{"", "folder1"}, // empty folder should be general
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Should retrieve dashboards by plugin through a different function", func(t *testing.T) {
|
||||
mockStore.On("GetDashboardsByPluginID", mock.Anything, &dashboards.GetDashboardsByPluginIDQuery{
|
||||
PluginID: "slo",
|
||||
OrgID: 2, // retrieved from the signed in user
|
||||
}).Return([]*dashboards.Dashboard{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_REPOSITORY_PATH,
|
||||
Operator: "in",
|
||||
Values: []string{"slo"},
|
||||
},
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
|
||||
Operator: "in",
|
||||
Values: []string{"plugin"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Should retrieve dashboards by provisioner name through a different function", func(t *testing.T) {
|
||||
mockStore.On("GetProvisionedDashboardsByName", mock.Anything, "test").Return([]*dashboards.Dashboard{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
|
||||
Operator: "in",
|
||||
Values: []string{"file:test"}, // file prefix should be removed before going to legacy
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Should retrieve orphaned dashboards if provisioner not in is specified", func(t *testing.T) {
|
||||
mockStore.On("GetOrphanedProvisionedDashboards", mock.Anything, []string{"test", "test2"}).Return([]*dashboards.Dashboard{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
|
||||
Operator: string(selection.NotIn),
|
||||
Values: []string{"file:test", "file:test2"}, // file prefix should be removed before going to legacy
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
@ -125,7 +125,7 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo != nil && repo.Name == "plugin" {
|
||||
if repo != nil && repo.Name == dashboard.PluginIDRepoName {
|
||||
dto.PluginID = repo.Path
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,8 @@ type Store interface {
|
||||
GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error)
|
||||
GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error)
|
||||
GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error)
|
||||
GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*Dashboard, error)
|
||||
GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*Dashboard, error)
|
||||
SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error)
|
||||
SaveProvisionedDashboard(ctx context.Context, dash *Dashboard, provisioning *DashboardProvisioning) error
|
||||
UnprovisionDashboard(ctx context.Context, id int64) error
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.32.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
@ -18,6 +18,10 @@ type FakeDashboardProvisioning struct {
|
||||
func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteOrphanedProvisionedDashboards")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
@ -32,6 +36,10 @@ func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx con
|
||||
func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error {
|
||||
ret := _m.Called(ctx, dashboardID, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteProvisionedDashboard")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
|
||||
r0 = rf(ctx, dashboardID, orgID)
|
||||
@ -46,6 +54,10 @@ func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Cont
|
||||
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
|
||||
ret := _m.Called(ctx, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetProvisionedDashboardData")
|
||||
}
|
||||
|
||||
var r0 []*DashboardProvisioning
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok {
|
||||
@ -72,6 +84,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Con
|
||||
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
|
||||
ret := _m.Called(ctx, dashboardID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetProvisionedDashboardDataByDashboardID")
|
||||
}
|
||||
|
||||
var r0 *DashboardProvisioning
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok {
|
||||
@ -98,6 +114,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ct
|
||||
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) {
|
||||
ret := _m.Called(ctx, orgID, dashboardUID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetProvisionedDashboardDataByDashboardUID")
|
||||
}
|
||||
|
||||
var r0 *DashboardProvisioning
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok {
|
||||
@ -124,6 +144,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c
|
||||
func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveFolderForProvisionedDashboards")
|
||||
}
|
||||
|
||||
var r0 *folder.Folder
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) (*folder.Folder, error)); ok {
|
||||
@ -150,6 +174,10 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont
|
||||
func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) {
|
||||
ret := _m.Called(ctx, dto, provisioning)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveProvisionedDashboard")
|
||||
}
|
||||
|
||||
var r0 *Dashboard
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) (*Dashboard, error)); ok {
|
||||
@ -176,6 +204,10 @@ func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Contex
|
||||
func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, dashboardID int64) error {
|
||||
ret := _m.Called(ctx, dashboardID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UnprovisionDashboard")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, dashboardID)
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
@ -74,6 +74,34 @@ func (_m *FakeDashboardService) CleanUpDeletedDashboards(ctx context.Context) (i
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CountDashboardsInOrg")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
|
||||
return rf(ctx, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
|
||||
r0 = rf(ctx, orgID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user
|
||||
func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID, folderUIDs, user)
|
||||
@ -102,6 +130,24 @@ func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) DeleteAllDashboards(ctx context.Context, orgID int64) error {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAllDashboards")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, orgID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteDashboard provides a mock function with given fields: ctx, dashboardId, dashboardUID, orgId
|
||||
func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
|
||||
ret := _m.Called(ctx, dashboardId, dashboardUID, orgId)
|
||||
@ -120,24 +166,6 @@ func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) DeleteAllDashboards(ctx context.Context, orgID int64) error {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteDashboard")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, orgID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// FindDashboards provides a mock function with given fields: ctx, query
|
||||
func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
@ -168,36 +196,6 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindP
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CountDashboardsInOrg")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
|
||||
return rf(ctx, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
|
||||
r0 = rf(ctx, orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetAllDashboards provides a mock function with given fields: ctx
|
||||
func (_m *FakeDashboardService) GetAllDashboards(ctx context.Context) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx)
|
||||
@ -228,6 +226,7 @@ func (_m *FakeDashboardService) GetAllDashboards(ctx context.Context) ([]*Dashbo
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetAllDashboardsByOrgId provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
|
@ -146,6 +146,38 @@ func (d *dashboardStore) GetProvisionedDashboardData(ctx context.Context, name s
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *dashboardStore) GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*dashboards.Dashboard, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.GetProvisionedDashboardsByName")
|
||||
defer span.End()
|
||||
|
||||
dashes := []*dashboards.Dashboard{}
|
||||
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.Table(`dashboard`).
|
||||
Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`).
|
||||
Where(`dashboard_provisioning.name = ?`, name).Find(&dashes)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dashes, nil
|
||||
}
|
||||
|
||||
func (d *dashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*dashboards.Dashboard, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.GetOrphanedProvisionedDashboards")
|
||||
defer span.End()
|
||||
|
||||
dashes := []*dashboards.Dashboard{}
|
||||
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.Table(`dashboard`).
|
||||
Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`).
|
||||
NotIn(`dashboard_provisioning.name`, notIn).Find(&dashes)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dashes, nil
|
||||
}
|
||||
|
||||
func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, dash *dashboards.Dashboard, provisioning *dashboards.DashboardProvisioning) error {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.SaveProvisionedDashboard")
|
||||
defer span.End()
|
||||
|
@ -251,26 +251,51 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
|
||||
|
||||
t.Run("Should delete associated provisioning info, even without the dashboard existing in the db", func(t *testing.T) {
|
||||
setup()
|
||||
dash1 := insertTestDashboard(t, dashboardStore, "provisioned", 1, 0, "", false, "provisioned")
|
||||
dash2 := insertTestDashboard(t, dashboardStore, "orphaned", 1, 0, "", false, "orphaned")
|
||||
provisioningData := &dashboards.DashboardProvisioning{
|
||||
ID: 1,
|
||||
DashboardID: 200,
|
||||
DashboardID: dash1.ID,
|
||||
Name: "test",
|
||||
CheckSum: "123",
|
||||
Updated: 54321,
|
||||
ExternalID: "/path/to/dashboard",
|
||||
}
|
||||
err := dashboardStore.SaveProvisionedDashboard(context.Background(), &dashboards.Dashboard{
|
||||
ID: 200,
|
||||
}, provisioningData)
|
||||
err := dashboardStore.SaveProvisionedDashboard(context.Background(), dash1, provisioningData)
|
||||
require.NoError(t, err)
|
||||
provisioningData2 := &dashboards.DashboardProvisioning{
|
||||
ID: 1,
|
||||
DashboardID: dash2.ID,
|
||||
Name: "orphaned",
|
||||
CheckSum: "123",
|
||||
Updated: 54321,
|
||||
ExternalID: "/path/to/dashboard",
|
||||
}
|
||||
err = dashboardStore.SaveProvisionedDashboard(context.Background(), dash2, provisioningData2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get provisioning data
|
||||
res, err := dashboardStore.GetProvisionedDashboardData(context.Background(), "test")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, res[0], provisioningData)
|
||||
|
||||
// get dashboards within the provisioner
|
||||
dashs, err := dashboardStore.GetProvisionedDashboardsByName(context.Background(), "test")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dashs, 1)
|
||||
|
||||
// find dashboards not within that provisioner
|
||||
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dashs, 1)
|
||||
// if both are provided, nothing should be returned
|
||||
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test", "orphaned"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dashs, 0)
|
||||
|
||||
err = dashboardStore.CleanupAfterDelete(context.Background(), &dashboards.DeleteDashboardCommand{
|
||||
ID: 200,
|
||||
ID: dash1.ID,
|
||||
OrgID: 1,
|
||||
UID: "test",
|
||||
})
|
||||
|
@ -958,7 +958,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||
OrgId: query.OrgID,
|
||||
ProvisionedRepo: pluginIDRepoName,
|
||||
ProvisionedRepo: dashboard.PluginIDRepoName,
|
||||
ProvisionedPath: query.PluginID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -1559,7 +1559,7 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C
|
||||
delete(annotations, utils.AnnoKeyRepoHash)
|
||||
delete(annotations, utils.AnnoKeyRepoTimestamp)
|
||||
} else {
|
||||
annotations[utils.AnnoKeyRepoName] = provisionedFileNameWithPrefix(provisioning.Name)
|
||||
annotations[utils.AnnoKeyRepoName] = dashboard.ProvisionedFileNameWithPrefix(provisioning.Name)
|
||||
annotations[utils.AnnoKeyRepoPath] = provisioning.ExternalID
|
||||
annotations[utils.AnnoKeyRepoHash] = provisioning.CheckSum
|
||||
annotations[utils.AnnoKeyRepoTimestamp] = time.Unix(provisioning.Updated, 0).UTC().Format(time.RFC3339)
|
||||
@ -1580,7 +1580,7 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setPluginID(obj, cmd.PluginID)
|
||||
dashboard.SetPluginIDMeta(obj, cmd.PluginID)
|
||||
|
||||
out, err := dr.createOrUpdateDash(ctx, obj, orgID)
|
||||
if err != nil {
|
||||
@ -1827,13 +1827,13 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, query.OrgId)
|
||||
|
||||
if query.ProvisionedRepo != "" {
|
||||
query.ProvisionedRepo = provisionedFileNameWithPrefix(query.ProvisionedRepo)
|
||||
query.ProvisionedRepo = dashboard.ProvisionedFileNameWithPrefix(query.ProvisionedRepo)
|
||||
}
|
||||
|
||||
if len(query.ProvisionedReposNotIn) > 0 {
|
||||
repos := make([]string, len(query.ProvisionedReposNotIn))
|
||||
for i, v := range query.ProvisionedReposNotIn {
|
||||
repos[i] = provisionedFileNameWithPrefix(v)
|
||||
repos[i] = dashboard.ProvisionedFileNameWithPrefix(v)
|
||||
}
|
||||
query.ProvisionedReposNotIn = repos
|
||||
}
|
||||
@ -1865,7 +1865,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
|
||||
}
|
||||
|
||||
// ensure the repo is set due to file provisioning, otherwise skip it
|
||||
fileRepo, found := getProvisionedFileNameFromMeta(meta)
|
||||
fileRepo, found := dashboard.GetProvisionedFileNameFromMeta(meta.GetRepositoryName())
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
@ -1967,7 +1967,7 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
|
||||
out.Deleted = obj.GetDeletionTimestamp().Time
|
||||
}
|
||||
|
||||
out.PluginID = GetPluginIDFromMeta(obj)
|
||||
out.PluginID = dashboard.GetPluginIDFromMeta(obj)
|
||||
|
||||
creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
|
||||
if err != nil {
|
||||
@ -2012,42 +2012,6 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
var pluginIDRepoName = "plugin"
|
||||
var fileProvisionedRepoPrefix = "file:"
|
||||
|
||||
func setPluginID(obj unstructured.Unstructured, pluginID string) {
|
||||
if pluginID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[utils.AnnoKeyRepoName] = pluginIDRepoName
|
||||
annotations[utils.AnnoKeyRepoPath] = pluginID
|
||||
obj.SetAnnotations(annotations)
|
||||
}
|
||||
|
||||
func provisionedFileNameWithPrefix(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fileProvisionedRepoPrefix + name
|
||||
}
|
||||
|
||||
func getProvisionedFileNameFromMeta(obj utils.GrafanaMetaAccessor) (string, bool) {
|
||||
return strings.CutPrefix(obj.GetRepositoryName(), fileProvisionedRepoPrefix)
|
||||
}
|
||||
|
||||
func GetPluginIDFromMeta(obj utils.GrafanaMetaAccessor) string {
|
||||
if obj.GetRepositoryName() == pluginIDRepoName {
|
||||
return obj.GetRepositoryPath()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, namespace string) (unstructured.Unstructured, error) {
|
||||
uid := cmd.GetDashboardModel().UID
|
||||
if uid == "" {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
@ -547,7 +548,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
|
||||
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
|
||||
},
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -563,7 +564,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
|
||||
k8sCliMock.On("Search", mock.Anything, int64(1),
|
||||
mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
|
||||
// ensure the prefix is added to the query
|
||||
return req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix(repo)
|
||||
return req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix(repo)
|
||||
})).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{},
|
||||
@ -573,7 +574,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
|
||||
}, nil).Once()
|
||||
k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
|
||||
// ensure the prefix is added to the query
|
||||
return req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix(repo)
|
||||
return req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix(repo)
|
||||
})).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
@ -646,7 +647,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
|
||||
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
|
||||
},
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -736,7 +737,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
|
||||
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
|
||||
},
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -825,7 +826,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "orphaned",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("orphaned"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -838,7 +839,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"name": "uid2",
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: "plugin",
|
||||
utils.AnnoKeyRepoName: dashboard.PluginIDRepoName,
|
||||
utils.AnnoKeyRepoHash: "app",
|
||||
},
|
||||
},
|
||||
@ -849,7 +850,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"name": "uid3",
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "orphaned",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("orphaned"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -858,7 +859,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
|
||||
"spec": map[string]any{},
|
||||
}}, nil).Once()
|
||||
k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
|
||||
})).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
@ -888,7 +889,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
|
||||
}, nil).Once()
|
||||
|
||||
k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
|
||||
})).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
@ -959,7 +960,7 @@ func TestUnprovisionDashboard(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
@ -1053,7 +1054,7 @@ func TestGetDashboardsByPluginID(t *testing.T) {
|
||||
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil)
|
||||
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
|
||||
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == "plugin" &&
|
||||
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.PluginIDRepoName &&
|
||||
req.Options.Fields[1].Key == "repo.path" && req.Options.Fields[1].Values[0] == "testing"
|
||||
})).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
@ -1983,7 +1984,7 @@ func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"annotations": map[string]any{
|
||||
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
|
||||
utils.AnnoKeyRepoHash: "hash",
|
||||
utils.AnnoKeyRepoPath: "path/to/file",
|
||||
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
@ -66,36 +66,6 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountInOrg provides a mock function with given fields: _a0, _a1
|
||||
func (_m *FakeDashboardStore) CountInOrg(_a0 context.Context, _a1 int64) (int64, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountDashboardsInFolders provides a mock function with given fields: ctx, request
|
||||
func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) {
|
||||
ret := _m.Called(ctx, request)
|
||||
@ -124,6 +94,35 @@ func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, requ
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountInOrg provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
|
||||
return rf(ctx, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
|
||||
r0 = rf(ctx, orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
@ -436,6 +435,36 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetOrphanedProvisionedDashboards provides a mock function with given fields: ctx, notIn
|
||||
func (_m *FakeDashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx, notIn)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetOrphanedProvisionedDashboards")
|
||||
}
|
||||
|
||||
var r0 []*Dashboard
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*Dashboard, error)); ok {
|
||||
return rf(ctx, notIn)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []string) []*Dashboard); ok {
|
||||
r0 = rf(ctx, notIn)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*Dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok {
|
||||
r1 = rf(ctx, notIn)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetProvisionedDashboardData provides a mock function with given fields: ctx, name
|
||||
func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
|
||||
ret := _m.Called(ctx, name)
|
||||
@ -466,6 +495,36 @@ func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, n
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetProvisionedDashboardsByName provides a mock function with given fields: ctx, name
|
||||
func (_m *FakeDashboardStore) GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetProvisionedDashboardsByName")
|
||||
}
|
||||
|
||||
var r0 []*Dashboard
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*Dashboard, error)); ok {
|
||||
return rf(ctx, name)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []*Dashboard); ok {
|
||||
r0 = rf(ctx, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*Dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetProvisionedDataByDashboardID provides a mock function with given fields: ctx, dashboardID
|
||||
func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
|
||||
ret := _m.Called(ctx, dashboardID)
|
||||
|
Loading…
Reference in New Issue
Block a user