Provisioning: Fix Dashboard Creation For First-Level Repository Folders (#109962)

This commit is contained in:
Costa Alexoglou
2025-08-27 12:20:57 +02:00
committed by GitHub
parent fa831577f1
commit 9785e573aa
4 changed files with 103 additions and 0 deletions
@@ -65,6 +65,7 @@ func (f *parserFactory) GetParser(ctx context.Context, repo repository.Reader) (
},
urls: urls,
clients: clients,
config: config,
}, nil
}
@@ -75,6 +76,8 @@ type parser struct {
// for repositories that have URL support
urls repository.RepositoryWithURLs
config *provisioning.Repository
// ResourceClients give access to k8s apis
clients ResourceClients
}
@@ -192,6 +195,8 @@ func (r *parser) Parse(ctx context.Context, info *repository.FileInfo) (parsed *
dirPath := safepath.Dir(info.Path)
if dirPath != "" {
parsed.Meta.SetFolder(ParseFolder(dirPath, r.repo.Name).ID)
} else {
parsed.Meta.SetFolder(RootFolder(r.config))
}
}
obj.SetUID("") // clear identifiers
@@ -11,6 +11,7 @@ import (
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestParser(t *testing.T) {
@@ -27,6 +28,16 @@ func TestParser(t *testing.T) {
Name: "repo",
},
clients: clients,
config: &provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Namespace: "xxx",
Name: "repo",
},
Spec: provisioning.RepositorySpec{
Type: provisioning.LocalRepositoryType,
Sync: provisioning.SyncOptions{Target: provisioning.SyncTargetTypeFolder},
},
},
}
t.Run("invalid input", func(t *testing.T) {
@@ -93,4 +104,43 @@ spec:
require.Equal(t, "dashboard.grafana.app", dash.GVR.Group)
require.Equal(t, "v0alpha1", dash.GVR.Version)
})
t.Run("validate proper folder metadata is set", func(t *testing.T) {
testCases := []struct {
name string
filePath string
expectedFolder string
}{
{
name: "file in subdirectory should use parsed folder ID",
filePath: "team-a/testing-valid-dashboard.json",
expectedFolder: ParseFolder("team-a/", "repo").ID,
},
{
name: "file in first-level directory should use parent folder id",
filePath: "testing-valid-dashboard.json",
expectedFolder: parser.repo.Name,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dash, err := parser.Parse(context.Background(), &repository.FileInfo{
Path: tc.filePath,
Data: []byte(`apiVersion: dashboard.grafana.app/v0alpha1
kind: Dashboard
metadata:
name: test-dashboard
spec:
title: Test dashboard
`),
})
require.NoError(t, err)
require.Equal(t, tc.expectedFolder, dash.Meta.GetFolder(), "folder should match expected")
annotations := dash.Obj.GetAnnotations()
require.NotNil(t, annotations, "annotations should not be nil")
require.Equal(t, tc.expectedFolder, annotations["grafana.app/folder"], "folder annotation should match expected")
})
}
})
}
+20
View File
@@ -43,6 +43,8 @@ func TestIntegrationProvisioning_DeleteResources(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 3, len(dashboards.Items))
helper.validateManagedDashboardsFolderMetadata(t, ctx, repo, dashboards.Items)
folders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Equal(t, 2, len(folders.Items))
@@ -126,6 +128,13 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) {
ExpectedFolders: 0,
})
// Validate the dashboard metadata
dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Equal(t, 1, len(dashboards.Items))
helper.validateManagedDashboardsFolderMetadata(t, ctx, repo, dashboards.Items)
// Verify the original dashboard exists in Grafana (using the UID from all-panels.json)
const allPanelsUID = "n1jR8vnnz" // This is the UID from the all-panels.json file
obj, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{})
@@ -420,6 +429,17 @@ func TestIntegrationProvisioning_FilesOwnershipProtection(t *testing.T) {
ExpectedFolders: 2, // Total across both repos
})
allDashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
for _, dashboard := range allDashboards.Items {
annotations := dashboard.GetAnnotations()
// Expect to be managed by repo1 or repo2
managerID := annotations["grafana.app/managerId"]
if managerID != repo1 && managerID != repo2 {
t.Fatalf("dashboard %s is not managed by repo1 or repo2", dashboard.GetName())
}
}
t.Run("CREATE file with UID already owned by different repository - should fail", func(t *testing.T) {
// Try to create a dashboard in repo2 that has the same UID as the one in repo1
// The all-panels.json has UID "n1jR8vnnz" which is already owned by repo1
@@ -460,6 +460,34 @@ func (h *provisioningTestHelper) logRepositoryObject(t *testing.T, obj map[strin
}
}
// validateManagedDashboardsFolderMetadata validates the folder metadata
// of the managed dashboards.
// If folder is nested, folder annotations should not be empty.
// Also checks that the managerId property exists.
func (h *provisioningTestHelper) validateManagedDashboardsFolderMetadata(t *testing.T,
ctx context.Context, repoName string, dashboards []unstructured.Unstructured) {
t.Helper()
// Check if folder is nested or not.
// If not, folder annotations should be empty as we have an "instance" sync target
for _, d := range dashboards {
sourcePath, _, _ := unstructured.NestedString(d.Object, "metadata", "annotations", "grafana.app/sourcePath")
isNested := strings.Contains(sourcePath, "/")
folder, found, _ := unstructured.NestedString(d.Object, "metadata", "annotations", "grafana.app/folder")
if isNested {
require.True(t, found, "dashboard should have a folder annotation")
require.NotEmpty(t, folder, "dashboard should be in a non-empty folder")
} else {
require.False(t, found, "dashboard should not have a folder annotation")
}
managerID, _, _ := unstructured.NestedString(d.Object, "metadata", "annotations", "grafana.app/managerId")
// require.Equal(t, repoName, managerID, "dashboard should be managed by gitsync repo")
require.Equal(t, repoName, managerID, "dashboard should be managed by gitsync repo")
}
}
type TestRepo struct {
Name string
Target string