Provisioning: Add resource-level warning support. (#115023)

This commit is contained in:
Gonzalo Trigueros Manzanas
2025-12-12 15:59:45 +01:00
committed by GitHub
parent 46ef9aaa0a
commit d4a627c5fc
10 changed files with 314 additions and 27 deletions
@@ -35,12 +35,13 @@ func maybeNotifyProgress(threshold time.Duration, fn ProgressFn) ProgressFn {
// FIXME: ProgressRecorder should be initialized in the queue
type JobResourceResult struct {
Name string
Group string
Kind string
Path string
Action repository.FileAction
Error error
Name string
Group string
Kind string
Path string
Action repository.FileAction
Error error
Warning error
}
type jobProgressRecorder struct {
@@ -193,6 +194,10 @@ func (r *jobProgressRecorder) updateSummary(result JobResourceResult) {
errorMsg := fmt.Sprintf("%s (file: %s, name: %s, action: %s)", result.Error.Error(), result.Path, result.Name, result.Action)
summary.Errors = append(summary.Errors, errorMsg)
summary.Error++
} else if result.Warning != nil {
warningMsg := fmt.Sprintf("%s (file: %s, name: %s, action: %s)", result.Warning.Error(), result.Path, result.Name, result.Action)
summary.Warnings = append(summary.Warnings, warningMsg)
summary.Warning++
} else {
switch result.Action {
case repository.FileActionDeleted:
@@ -266,8 +271,17 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision
jobStatus.Message = err.Error()
}
jobStatus.Summary = r.summary()
summaries := r.summary()
jobStatus.Summary = summaries
jobStatus.Errors = r.errors
// Extract warnings from summaries
warnings := make([]string, 0)
for _, summary := range summaries {
warnings = append(warnings, summary.Warnings...)
}
jobStatus.Warnings = warnings
jobStatus.URLs = r.refURLs
tooManyErrors := r.maxErrors > 0 && r.errorCount >= r.maxErrors
@@ -283,6 +297,9 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision
jobStatus.Message = "completed with errors"
jobStatus.State = provisioning.JobStateWarning
}
} else if len(jobStatus.Warnings) > 0 {
jobStatus.State = provisioning.JobStateWarning
jobStatus.Message = "completed with warnings"
}
// Override message if progress have a more explicit message
@@ -2,9 +2,11 @@ package jobs
import (
"context"
"errors"
"testing"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -83,3 +85,167 @@ func TestJobProgressRecorderCompleteIncludesRefURLs(t *testing.T) {
assert.Equal(t, provisioning.JobStateSuccess, finalStatus.State)
assert.Equal(t, "completed successfully", finalStatus.Message)
}
func TestJobProgressRecorderWarningStatus(t *testing.T) {
ctx := context.Background()
// Create a progress recorder
mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error {
return nil
}
recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder)
// Record a result with a warning
warningErr := errors.New("deprecated API used")
result := JobResourceResult{
Name: "test-resource",
Group: "test.grafana.app",
Kind: "Dashboard",
Path: "dashboards/test.json",
Action: repository.FileActionUpdated,
Warning: warningErr,
}
recorder.Record(ctx, result)
// Record another result with a different warning
warningErr2 := errors.New("missing optional field")
result2 := JobResourceResult{
Name: "test-resource-2",
Group: "test.grafana.app",
Kind: "Dashboard",
Path: "dashboards/test2.json",
Action: repository.FileActionCreated,
Warning: warningErr2,
}
recorder.Record(ctx, result2)
// Record a result with a warning from a different resource type
warningErr3 := errors.New("validation warning")
result3 := JobResourceResult{
Name: "test-resource-3",
Group: "test.grafana.app",
Kind: "DataSource",
Path: "datasources/test.yaml",
Action: repository.FileActionCreated,
Warning: warningErr3,
}
recorder.Record(ctx, result3)
// Verify warnings are stored in summaries
recorder.mu.RLock()
require.Len(t, recorder.summaries, 2) // Dashboard and DataSource
dashboardSummary := recorder.summaries["test.grafana.app:Dashboard"]
require.NotNil(t, dashboardSummary)
assert.Equal(t, int64(2), dashboardSummary.Warning)
assert.Len(t, dashboardSummary.Warnings, 2)
assert.Contains(t, dashboardSummary.Warnings[0], "deprecated API used")
assert.Contains(t, dashboardSummary.Warnings[1], "missing optional field")
datasourceSummary := recorder.summaries["test.grafana.app:DataSource"]
require.NotNil(t, datasourceSummary)
assert.Equal(t, int64(1), datasourceSummary.Warning)
assert.Len(t, datasourceSummary.Warnings, 1)
assert.Contains(t, datasourceSummary.Warnings[0], "validation warning")
recorder.mu.RUnlock()
// Complete the job
finalStatus := recorder.Complete(ctx, nil)
// Verify the final status includes warnings
require.NotNil(t, finalStatus.Warnings)
assert.Len(t, finalStatus.Warnings, 3)
assert.Contains(t, finalStatus.Warnings[0], "deprecated API used")
assert.Contains(t, finalStatus.Warnings[1], "missing optional field")
assert.Contains(t, finalStatus.Warnings[2], "validation warning")
// Verify the state is set to Warning
assert.Equal(t, provisioning.JobStateWarning, finalStatus.State)
assert.Equal(t, "completed with warnings", finalStatus.Message)
// Verify summaries are included
require.Len(t, finalStatus.Summary, 2)
// Verify no errors were recorded
assert.Empty(t, finalStatus.Errors)
}
func TestJobProgressRecorderWarningWithErrors(t *testing.T) {
ctx := context.Background()
// Create a progress recorder
mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error {
return nil
}
recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder)
// Record a result with an error (errors take precedence)
errorErr := errors.New("failed to process")
result := JobResourceResult{
Name: "test-resource",
Group: "test.grafana.app",
Kind: "Dashboard",
Path: "dashboards/test.json",
Action: repository.FileActionUpdated,
Error: errorErr,
}
recorder.Record(ctx, result)
// Record a result with only a warning
warningErr := errors.New("deprecated API used")
result2 := JobResourceResult{
Name: "test-resource-2",
Group: "test.grafana.app",
Kind: "Dashboard",
Path: "dashboards/test2.json",
Action: repository.FileActionCreated,
Warning: warningErr,
}
recorder.Record(ctx, result2)
// Complete the job
finalStatus := recorder.Complete(ctx, nil)
// When there are errors, the state should be Warning (not Error unless too many)
// and warnings should still be included
assert.Equal(t, provisioning.JobStateWarning, finalStatus.State)
assert.Equal(t, "completed with errors", finalStatus.Message)
assert.Len(t, finalStatus.Errors, 1)
assert.Contains(t, finalStatus.Errors[0], "failed to process")
// Warnings should still be extracted from summaries
require.NotNil(t, finalStatus.Warnings)
assert.Len(t, finalStatus.Warnings, 1)
assert.Contains(t, finalStatus.Warnings[0], "deprecated API used")
}
func TestJobProgressRecorderWarningOnlyNoErrors(t *testing.T) {
ctx := context.Background()
// Create a progress recorder
mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error {
return nil
}
recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder)
// Record only warnings, no errors
warningErr := errors.New("deprecated API used")
result := JobResourceResult{
Name: "test-resource",
Group: "test.grafana.app",
Kind: "Dashboard",
Path: "dashboards/test.json",
Action: repository.FileActionUpdated,
Warning: warningErr,
}
recorder.Record(ctx, result)
// Complete the job
finalStatus := recorder.Complete(ctx, nil)
// Verify the state is Warning (not Error) when only warnings exist
assert.Equal(t, provisioning.JobStateWarning, finalStatus.State)
assert.Equal(t, "completed with warnings", finalStatus.Message)
assert.Empty(t, finalStatus.Errors)
require.NotNil(t, finalStatus.Warnings)
assert.Len(t, finalStatus.Warnings, 1)
}
@@ -3696,7 +3696,7 @@
"format": "int64"
},
"errors": {
"description": "Report errors for this resource type This may not be an exhaustive list and recommend looking at the logs for more info",
"description": "Report errors/warnings for this resource type This may not be an exhaustive list and recommend looking at the logs for more info",
"type": "array",
"items": {
"type": "string",
@@ -3722,6 +3722,18 @@
"type": "integer",
"format": "int64"
},
"warning": {
"description": "The error count",
"type": "integer",
"format": "int64"
},
"warnings": {
"type": "array",
"items": {
"type": "string",
"default": ""
}
},
"write": {
"type": "integer",
"format": "int64"
@@ -3849,6 +3861,13 @@
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.RepositoryURLs"
}
]
},
"warnings": {
"type": "array",
"items": {
"type": "string",
"default": ""
}
}
}
},