package resources import ( "context" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) // Get repository stats // //go:generate mockery --name ResourceLister --structname MockResourceLister --inpackage --filename resource_lister_mock.go --with-expecter type ResourceLister interface { List(ctx context.Context, namespace, repository string) (*provisioning.ResourceList, error) Stats(ctx context.Context, namespace, repository string) (*provisioning.ResourceStats, error) } type ResourceStore interface { resourcepb.ManagedObjectIndexClient resourcepb.ResourceIndexClient } type ResourceListerFromSearch struct { store ResourceStore legacyMigrator legacy.LegacyMigrator storageStatus dualwrite.Service } func NewResourceLister(store ResourceStore) ResourceLister { return &ResourceListerFromSearch{store: store} } // FIXME: the logic about migration and storage should probably be separated from this func NewResourceListerForMigrations( store ResourceStore, legacyMigrator legacy.LegacyMigrator, storageStatus dualwrite.Service, ) ResourceLister { return &ResourceListerFromSearch{ store: store, legacyMigrator: legacyMigrator, storageStatus: storageStatus, } } // List implements ResourceLister. func (o *ResourceListerFromSearch) List(ctx context.Context, namespace, repository string) (*provisioning.ResourceList, error) { objects, err := o.store.ListManagedObjects(ctx, &resourcepb.ListManagedObjectsRequest{ Namespace: namespace, Kind: string(utils.ManagerKindRepo), Id: repository, }) if err != nil { return nil, err } if objects.Error != nil { return nil, resource.GetError(objects.Error) } list := &provisioning.ResourceList{} for _, v := range objects.Items { list.Items = append(list.Items, provisioning.ResourceListItem{ Path: v.Path, Group: v.Object.Group, Resource: v.Object.Resource, Name: v.Object.Name, Hash: v.Hash, Time: v.Time, Title: v.Title, Folder: v.Folder, }) } return list, nil } // Stats implements ResourceLister. func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, repository string) (*provisioning.ResourceStats, error) { req := &resourcepb.CountManagedObjectsRequest{ Namespace: namespace, } if repository != "" { req.Kind = string(utils.ManagerKindRepo) req.Id = repository } counts, err := o.store.CountManagedObjects(ctx, req) if err != nil { return nil, err } if counts.Error != nil { return nil, resource.GetError(counts.Error) } lookup := make(map[string]*provisioning.ManagerStats) for _, v := range counts.Items { key := v.Kind + ":" + v.Id m := lookup[key] if m == nil { m = &provisioning.ManagerStats{ Kind: utils.ManagerKind(v.Kind), Identity: v.Id, } lookup[key] = m } m.Stats = append(m.Stats, provisioning.ResourceCount{ Group: v.Group, Resource: v.Resource, Count: v.Count, }) } stats := &provisioning.ResourceStats{ TypeMeta: metav1.TypeMeta{ APIVersion: provisioning.SchemeGroupVersion.String(), Kind: "ResourceStats", }, } for _, v := range lookup { stats.Managed = append(stats.Managed, *v) } // When selecting an explicit repository, do not fetch global stats if repository != "" { return stats, nil } // Get the stats based on what a migration could support if o.storageStatus != nil && o.legacyMigrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { rsp, err := o.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ Namespace: namespace, Resources: []schema.GroupResource{{ Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, }, { Group: folders.GROUP, Resource: folders.RESOURCE, }}, WithHistory: false, OnlyCount: true, }) if err != nil { return nil, err } for _, v := range rsp.Summary { stats.Instance = append(stats.Instance, provisioning.ResourceCount{ Group: v.Group, Resource: v.Resource, Count: v.Count, }) // Everything is unmanaged in legacy storage stats.Unmanaged = append(stats.Unmanaged, provisioning.ResourceCount{ Group: v.Group, Resource: v.Resource, Count: v.Count, }) } return stats, nil } // Get full instance stats info, err := o.store.GetStats(ctx, &resourcepb.ResourceStatsRequest{ Namespace: namespace, }) if err != nil { return nil, err } // Create a map to track managed counts by group/resource managedCounts := make(map[string]int64) for _, manager := range stats.Managed { for _, managedStat := range manager.Stats { key := managedStat.Group + ":" + managedStat.Resource managedCounts[key] += managedStat.Count } } for _, v := range info.Stats { stats.Instance = append(stats.Instance, provisioning.ResourceCount{ Group: v.Group, Resource: v.Resource, Count: v.Count, }) // Calculate unmanaged count: total - managed key := v.Group + ":" + v.Resource managedCount := managedCounts[key] unmanagedCount := v.Count - managedCount if unmanagedCount > 0 { stats.Unmanaged = append(stats.Unmanaged, provisioning.ResourceCount{ Group: v.Group, Resource: v.Resource, Count: unmanagedCount, }) } } return stats, nil }