diff --git a/pkg/apis/dashboard/utils.go b/pkg/apis/dashboard/utils.go new file mode 100644 index 00000000000..a7fa10af2d0 --- /dev/null +++ b/pkg/apis/dashboard/utils.go @@ -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: +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 "" +} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 6c1553f9bad..0ba1e67cc88 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -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! diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go new file mode 100644 index 00000000000..7a75f7acf3b --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -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 + }) +} diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index c0e18288ea5..50ada42a4c5 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -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 { diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go new file mode 100644 index 00000000000..0a2e2d4ca5b --- /dev/null +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -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) + }) +} diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index e48970a7344..a3c930694ea 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -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 } diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 8d56c056afa..2561b74e987 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -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 diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index a743752fcd5..ce7893a24a9 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -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) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index af8a6184e2a..2e37ea6f3b8 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -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) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index a9dd51c56a0..06c0c1a0550 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -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() diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index ff33a306a9e..79b94fa7ab7 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -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", }) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ed8a8fa1d19..eea8dd574ad 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -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 == "" { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index ccfa8f9e3a3..68ebc0ac61a 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -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", diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index eed18fda8db..508fece061c 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -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)