Provisioning: Mark repository as unhealthy if hooks fail (#109788)

This commit is contained in:
Roberto Jiménez Sánchez
2025-08-21 08:32:23 +00:00
committed by GitHub
parent 03d9ec4ea3
commit 61d137992b
22 changed files with 1578 additions and 490 deletions
+75 -199
View File
@@ -40,11 +40,11 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
for _, inputFilePath := range inputFiles {
t.Run(inputFilePath, func(t *testing.T) {
input := helper.RenderObject(t, inputFilePath, nil)
name := mustNestedString(input.Object, "metadata", "name")
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
require.NoError(t, err, "failed to create resource")
name := mustNestedString(input.Object, "metadata", "name")
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
require.NoError(t, err, "failed to read back resource")
@@ -257,18 +257,15 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) {
// )
const repo = "github-create-test"
_, err := helper.Repositories.Resource.Create(ctx,
helper.RenderObject(t, "testdata/github-readonly.json.tmpl", map[string]any{
"Name": repo,
"SyncEnabled": true,
"SyncTarget": "instance",
"Path": "grafana/",
}),
metav1.CreateOptions{},
)
require.NoError(t, err)
testRepo := TestRepo{
Name: repo,
Template: "testdata/github-readonly.json.tmpl",
Target: "folder",
ExpectedDashboards: 3,
ExpectedFolders: 3, // Folder sync creates an additional folder for the repository itself
}
helper.SyncAndWait(t, repo, nil)
helper.CreateRepo(t, testRepo)
// By now, we should have synced, meaning we have data to read in the local Grafana instance!
@@ -324,10 +321,12 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Create repository directly without health checks since we're only testing URL cleanup
input := helper.RenderObject(t, "testdata/github-readonly.json.tmpl", map[string]any{
"Name": test.name,
"URL": test.input,
"SyncTarget": "instance",
"Name": test.name,
"URL": test.input,
"SyncTarget": "folder",
"SyncEnabled": false, // Disable sync since we're just testing URL cleanup
})
_, err := helper.Repositories.Resource.Create(ctx, input, metav1.CreateOptions{})
@@ -353,7 +352,7 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) {
})
}
func TestIntegrationProvisioning_InstanceSyncValidation(t *testing.T) {
func TestIntegrationProvisioning_RepositoryLimits(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -361,136 +360,20 @@ func TestIntegrationProvisioning_InstanceSyncValidation(t *testing.T) {
helper := runGrafana(t)
ctx := context.Background()
t.Run("single instance sync is allowed", func(t *testing.T) {
repoName := "instance-repo-single"
testRepo := TestRepo{
Name: repoName,
Target: "instance",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
// Create instance sync repository - should succeed
helper.CreateRepo(t, testRepo)
// Clean up at end of test
helper.CleanupAllRepos(t)
})
t.Run("change between folder and instance sync for the same repository if no previous sync happened", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
repoName := "instance-repo-change"
testRepo := TestRepo{
Name: repoName,
Target: "instance",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 0,
SkipSync: true, // To avoid initial sync and stats
}
helper.CreateRepo(t, testRepo)
// Change from instance to folder sync
repo, err := helper.Repositories.Resource.Get(ctx, repoName, metav1.GetOptions{})
require.NoError(t, err, "failed to get repository")
err = unstructured.SetNestedField(repo.Object, "folder", "spec", "sync", "target")
require.NoError(t, err, "failed to set syncTarget to folder")
_, err = helper.Repositories.Resource.Update(ctx, repo, metav1.UpdateOptions{FieldValidation: "Strict"})
require.NoError(t, err, "failed to update repository to folder sync")
// Clean up at end of test
helper.CleanupAllRepos(t)
})
t.Run("instance sync rejected when any other repository exists", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
existingFolderName := "existing-folder-repo"
instanceRepoName := "instance-repo-blocked"
// Create a folder sync repository first
folderTestRepo := TestRepo{
Name: existingFolderName,
Target: "folder",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 1, // One folder expected after sync
}
helper.CreateRepo(t, folderTestRepo)
// Try to create an instance sync repository - should fail because any other repository exists
instanceRepo := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{
"Name": instanceRepoName,
"SyncEnabled": true,
"SyncTarget": "instance",
})
_, err := helper.Repositories.Resource.Create(ctx, instanceRepo, metav1.CreateOptions{FieldValidation: "Strict"})
require.Error(t, err, "instance sync repository should be rejected when any other repository exists")
// Verify the error message mentions that instance can only be created when no other repositories exist
statusError := helper.RequireApiErrorStatus(err, metav1.StatusReasonInvalid, http.StatusUnprocessableEntity)
require.Contains(t, statusError.Message, "Instance repository can only be created when no other repositories exist. Found: "+existingFolderName)
// Clean up at end of test
helper.CleanupAllRepos(t)
})
t.Run("multiple folder syncs are allowed", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
firstFolderName := "folder-repo-multi-1"
secondFolderName := "folder-repo-multi-2"
// Create first folder sync repository
folderTestRepo1 := TestRepo{
Name: firstFolderName,
Target: "folder",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 1, // One folder expected after sync
}
helper.CreateRepo(t, folderTestRepo1)
// Create second folder sync repository - should succeed
folderTestRepo2 := TestRepo{
Name: secondFolderName,
Target: "folder",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 2, // Two folders expected after sync (1 + 1)
}
helper.CreateRepo(t, folderTestRepo2)
// Clean up at end of test
helper.CleanupAllRepos(t)
})
originalName := "original-repo"
// Create instance sync repository first
originalRepo := TestRepo{
Name: originalName,
Target: "instance",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, originalRepo)
t.Run("folder sync is rejected when instance sync exists", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
instanceRepoName := "instance-blocking-folder"
folderRepoName := "folder-blocked-by-instance"
// Create instance sync repository first
instanceTestRepo := TestRepo{
Name: instanceRepoName,
Target: "instance",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, instanceTestRepo)
// Try to create folder sync repository - should fail
folderRepo := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{
"Name": folderRepoName,
"Name": "folder-blocked-by-instance",
"SyncEnabled": true,
"SyncTarget": "folder",
})
@@ -500,55 +383,56 @@ func TestIntegrationProvisioning_InstanceSyncValidation(t *testing.T) {
// Verify the error message mentions the existing instance repository
statusError := helper.RequireApiErrorStatus(err, metav1.StatusReasonInvalid, http.StatusUnprocessableEntity)
require.Contains(t, statusError.Message, "Cannot create folder repository when instance repository exists: "+instanceRepoName)
// Clean up at end of test
helper.CleanupAllRepos(t)
require.Contains(t, statusError.Message, "Cannot create folder repository when instance repository exists: "+originalName)
})
t.Run("instance sync can only be created when no repositories exist", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
t.Run("change between folder and instance sync for the same repository if no previous sync happened", func(t *testing.T) {
repo, err := helper.Repositories.Resource.Get(ctx, originalName, metav1.GetOptions{})
require.NoError(t, err, "failed to get repository")
err = unstructured.SetNestedField(repo.Object, "folder", "spec", "sync", "target")
require.NoError(t, err, "failed to set syncTarget to folder")
_, err = helper.Repositories.Resource.Update(ctx, repo, metav1.UpdateOptions{FieldValidation: "Strict"})
require.NoError(t, err, "failed to update repository to folder sync")
// This test verifies that instance sync can ONLY be created when there are no other repositories
instanceRepoName := "instance-only-when-empty"
// Verify that the repository is now a folder sync
// We verify with the listing APIs because it may take some time for the update to propagate
require.Eventually(t, func() bool {
repos, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{})
if err != nil {
return false
}
// First, create instance sync repository when no other repositories exist - should succeed
instanceTestRepo := TestRepo{
Name: instanceRepoName,
Target: "instance",
Copies: map[string]string{}, // No files needed for this test
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, instanceTestRepo)
for _, repo := range repos.Items {
if repo.GetName() == originalName {
syncTarget, found, err := unstructured.NestedString(repo.Object, "spec", "sync", "target")
if err != nil || !found {
return false
}
// Now try to create any other repository - should fail
otherRepoName := "other-repo-blocked"
otherRepo := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{
"Name": otherRepoName,
return syncTarget == "folder"
}
}
return false
}, time.Second*10, time.Millisecond*100, "failed to verify that sync target is folder")
})
t.Run("instance sync rejected when any other repository exists", func(t *testing.T) {
instanceRepo := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{
"Name": "instance-repo-blocked",
"SyncEnabled": true,
"SyncTarget": "folder",
"SyncTarget": "instance",
})
_, err := helper.Repositories.Resource.Create(ctx, otherRepo, metav1.CreateOptions{FieldValidation: "Strict"})
require.Error(t, err, "folder sync repository should be rejected when instance sync exists")
_, err := helper.Repositories.Resource.Create(ctx, instanceRepo, metav1.CreateOptions{FieldValidation: "Strict"})
require.Error(t, err, "instance sync repository should be rejected when any other repository exists")
statusError := helper.RequireApiErrorStatus(err, metav1.StatusReasonInvalid, http.StatusUnprocessableEntity)
require.Contains(t, statusError.Message, "Cannot create folder repository when instance repository exists: "+instanceRepoName)
// Clean up at end of test
helper.CleanupAllRepos(t)
require.Contains(t, statusError.Message, "Instance repository can only be created when no other repositories exist. Found: "+originalName)
})
t.Run("repository limit validation", func(t *testing.T) {
// Ensure clean state
helper.CleanupAllRepos(t)
// This test verifies the 10 repository limit validation by actually creating 10 repositories
// Create 10 repositories - should all succeed
for i := 1; i <= 10; i++ {
t.Run("repository limit validation of 10 for folder syncs repositories", func(t *testing.T) {
for i := 2; i <= 10; i++ {
repoName := fmt.Sprintf("limit-test-repo-%d", i)
limitTestRepo := TestRepo{
Name: repoName,
@@ -571,12 +455,8 @@ func TestIntegrationProvisioning_InstanceSyncValidation(t *testing.T) {
_, err := helper.Repositories.Resource.Create(ctx, eleventhRepo, metav1.CreateOptions{FieldValidation: "Strict"})
require.Error(t, err, "11th repository should be rejected due to limit")
// Verify the error message mentions the repository limit
statusError := helper.RequireApiErrorStatus(err, metav1.StatusReasonInvalid, http.StatusUnprocessableEntity)
require.Contains(t, statusError.Message, "Maximum number of 10 repositories reached")
// Clean up at end of test
helper.CleanupAllRepos(t)
})
}
@@ -593,11 +473,7 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) {
const targetPath = "all-panels.json"
// Set up the repository.
localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{"Name": repo})
obj, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{})
require.NoError(t, err)
name, _, _ := unstructured.NestedString(obj.Object, "metadata", "name")
require.Equal(t, repo, name, "wrote the expected name")
helper.CreateRepo(t, TestRepo{Name: repo})
// Write a file -- this will create it *both* in the local file system, and in grafana
t.Run("write all panels", func(t *testing.T) {
@@ -702,7 +578,7 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) {
require.Error(t, result.Error(), "invalid path should return error")
// Read a file with a bad path
_, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "../../all-panels.json")
_, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "../../all-panels.json")
require.Error(t, err, "invalid path should error")
})
@@ -742,7 +618,7 @@ spec:
err = json.Unmarshal(raw, obj)
require.NoError(t, err)
name, _, _ = unstructured.NestedString(obj.Object, "resource", "upsert", "metadata", "name")
name, _, _ := unstructured.NestedString(obj.Object, "resource", "upsert", "metadata", "name")
require.True(t, strings.HasPrefix(name, "prefix-"), "should generate name")
})
}
@@ -763,15 +639,15 @@ func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository(t *testing.T
const repo = "local-tmp"
// Set up the repository and the file to import.
helper.CopyToProvisioningPath(t, "testdata/all-panels.json", "all-panels.json")
localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{
"Name": repo,
"SyncEnabled": true,
})
testRepo := TestRepo{
Name: repo,
Target: "instance",
Copies: map[string]string{"testdata/all-panels.json": "all-panels.json"},
ExpectedDashboards: 1,
ExpectedFolders: 0,
}
// We create the repository
_, err = helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{})
require.NoError(t, err)
helper.CreateRepo(t, testRepo)
// Now, we import it, such that it may exist
// The sync may not be necessary as the sync may have happened automatically at this point