Provisioning: Return unmanaged count in stats (#109462)
* Unmanaged counts * Add integration test * Generate open api spec
This commit is contained in:
@@ -439,6 +439,11 @@ type ResourceStats struct {
|
||||
// +listType=atomic
|
||||
Instance []ResourceCount `json:"instance,omitempty"`
|
||||
|
||||
// Stats across all unified storage
|
||||
// When legacy storage is still used, this will offer a shim
|
||||
// +listType=atomic
|
||||
Unmanaged []ResourceCount `json:"unmanaged,omitempty"`
|
||||
|
||||
// Stats for each manager
|
||||
// +listType=atomic
|
||||
Managed []ManagerStats `json:"managed,omitempty"`
|
||||
|
||||
@@ -974,6 +974,11 @@ func (in *ResourceStats) DeepCopyInto(out *ResourceStats) {
|
||||
*out = make([]ResourceCount, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Unmanaged != nil {
|
||||
in, out := &in.Unmanaged, &out.Unmanaged
|
||||
*out = make([]ResourceCount, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Managed != nil {
|
||||
in, out := &in.Managed, &out.Managed
|
||||
*out = make([]ManagerStats, len(*in))
|
||||
|
||||
@@ -2126,6 +2126,25 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref common.ReferenceCal
|
||||
},
|
||||
},
|
||||
},
|
||||
"unmanaged": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Stats across all unified storage When legacy storage is still used, this will offer a shim",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ResourceCount"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"managed": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
|
||||
@@ -146,6 +146,12 @@ func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, reposit
|
||||
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
|
||||
}
|
||||
@@ -157,12 +163,35 @@ func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, reposit
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5289,6 +5289,14 @@
|
||||
},
|
||||
"metadata": {
|
||||
"default": {}
|
||||
},
|
||||
"unmanaged": {
|
||||
"description": "Stats across all unified storage When legacy storage is still used, this will offer a shim",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {}
|
||||
},
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_Stats(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const repo = "stats-test-repo1"
|
||||
|
||||
testRepo := TestRepo{
|
||||
Name: repo,
|
||||
Copies: map[string]string{
|
||||
"testdata/all-panels.json": "dashboard1.json",
|
||||
"testdata/text-options.json": "folder/dashboard2.json",
|
||||
},
|
||||
ExpectedDashboards: 2,
|
||||
ExpectedFolders: 1,
|
||||
}
|
||||
helper.CreateRepo(t, testRepo)
|
||||
|
||||
// Create some unmanaged dashboards directly in Grafana
|
||||
unmanagedDash1 := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v1.yaml")
|
||||
dashboard1Obj, err := helper.DashboardsV1.Resource.Create(ctx, unmanagedDash1, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "should be able to create unmanaged dashboard")
|
||||
dashboard1Name := dashboard1Obj.GetName()
|
||||
|
||||
// Verify that the unmanaged dashboard is indeed unmanaged
|
||||
dashboard1, err := helper.DashboardsV1.Resource.Get(ctx, dashboard1Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
manager1, found1 := dashboard1.GetAnnotations()[utils.AnnoKeyManagerIdentity]
|
||||
require.True(t, !found1 || manager1 == "", "dashboard1 should be unmanaged")
|
||||
|
||||
// Get global stats
|
||||
result := helper.AdminREST.Get().
|
||||
Namespace("default").
|
||||
Resource("stats").
|
||||
Do(ctx)
|
||||
require.NoError(t, result.Error(), "should be able to get global stats")
|
||||
|
||||
statsObj, err := result.Get()
|
||||
require.NoError(t, err, "should be able to get global stats object")
|
||||
unstructuredStats := statsObj.(*unstructured.Unstructured)
|
||||
|
||||
// Parse instance stats
|
||||
instanceStats, _, err := unstructured.NestedSlice(unstructuredStats.Object, "instance")
|
||||
require.NoError(t, err, "should be able to get instance stats")
|
||||
|
||||
var dashboardInstanceCount int64
|
||||
for _, instanceStat := range instanceStats {
|
||||
stat := instanceStat.(map[string]interface{})
|
||||
group, _, _ := unstructured.NestedString(stat, "group")
|
||||
resource, _, _ := unstructured.NestedString(stat, "resource")
|
||||
if group == "dashboard.grafana.app" && resource == "dashboards" {
|
||||
count, _, _ := unstructured.NestedInt64(stat, "count")
|
||||
dashboardInstanceCount = count
|
||||
break
|
||||
}
|
||||
}
|
||||
require.GreaterOrEqual(t, dashboardInstanceCount, int64(3), "should have at least 3 dashboards total (2 managed + 1 unmanaged)")
|
||||
|
||||
// Parse managed stats
|
||||
managedStats, _, err := unstructured.NestedSlice(unstructuredStats.Object, "managed")
|
||||
require.NoError(t, err, "should be able to get managed stats")
|
||||
|
||||
var totalManagedDashboards int64
|
||||
foundRepo := false
|
||||
for _, manager := range managedStats {
|
||||
managerObj := manager.(map[string]interface{})
|
||||
identity, _, _ := unstructured.NestedString(managerObj, "id")
|
||||
|
||||
if identity == repo {
|
||||
foundRepo = true
|
||||
stats, _, _ := unstructured.NestedSlice(managerObj, "stats")
|
||||
for _, statObj := range stats {
|
||||
stat := statObj.(map[string]interface{})
|
||||
group, _, _ := unstructured.NestedString(stat, "group")
|
||||
resource, _, _ := unstructured.NestedString(stat, "resource")
|
||||
if group == "dashboard.grafana.app" && resource == "dashboards" {
|
||||
count, _, _ := unstructured.NestedInt64(stat, "count")
|
||||
totalManagedDashboards += count
|
||||
require.Equal(t, int64(2), count, "repo should manage 2 dashboards")
|
||||
} else if group == "folder.grafana.app" && resource == "folders" {
|
||||
count, _, _ := unstructured.NestedInt64(stat, "count")
|
||||
require.Equal(t, int64(1), count, "repo should manage 1 folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, foundRepo, "should find stats for repo1")
|
||||
|
||||
// Parse unmanaged stats
|
||||
unmanagedStats, _, err := unstructured.NestedSlice(unstructuredStats.Object, "unmanaged")
|
||||
require.NoError(t, err, "should be able to get unmanaged stats")
|
||||
|
||||
var unmanagedDashboardCount int64
|
||||
for _, unmanagedStat := range unmanagedStats {
|
||||
stat := unmanagedStat.(map[string]interface{})
|
||||
group, _, _ := unstructured.NestedString(stat, "group")
|
||||
resource, _, _ := unstructured.NestedString(stat, "resource")
|
||||
if group == "dashboard.grafana.app" && resource == "dashboards" {
|
||||
count, _, _ := unstructured.NestedInt64(stat, "count")
|
||||
unmanagedDashboardCount = count
|
||||
break
|
||||
}
|
||||
}
|
||||
require.GreaterOrEqual(t, unmanagedDashboardCount, int64(1), "should have at least 1 unmanaged dashboard")
|
||||
|
||||
// Verify the math: instance = managed + unmanaged
|
||||
require.Equal(t, dashboardInstanceCount, totalManagedDashboards+unmanagedDashboardCount,
|
||||
"instance count should equal managed + unmanaged counts")
|
||||
}
|
||||
Reference in New Issue
Block a user