Files
grafana/apps/provisioning/pkg/repository/validator.go
Roberto Jiménez Sánchez a0751b6e71 Provisioning: Default to folder sync only and block new instance sync repositories (#115569)
* Default to folder sync only and block new instance sync repositories

- Change default allowed_targets to folder-only in backend configuration
- Modify validation to only enforce allowedTargets on CREATE operations
- Add deprecation warning for existing instance sync repositories
- Update frontend defaults and tests to reflect new behavior

Fixes #619

* Update warning message: change 'deprecated' to 'not fully supported'

* Fix health check: don't validate allowedTargets for existing repositories

Health checks for existing repositories should treat them as UPDATE operations,
not CREATE operations, so they don't fail validation for instance sync target.

* Fix tests and update i18n translations

- Update BootstrapStep tests to reflect folder-only default behavior
- Run i18n-extract to update translation file structure

* Fix integration tests

* Fix tests

* Fix provisioning test wizard

* Fix fronted test
2025-12-19 11:44:15 +00:00

140 lines
4.7 KiB
Go

package repository
import (
"fmt"
"net/http"
"slices"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)
type RepositoryValidator struct {
allowedTargets []provisioning.SyncTargetType
allowImageRendering bool
minSyncInterval time.Duration
}
func NewValidator(minSyncInterval time.Duration, allowedTargets []provisioning.SyncTargetType, allowImageRendering bool) RepositoryValidator {
// do not allow minsync interval to be less than 10
if minSyncInterval <= 10*time.Second {
minSyncInterval = 10 * time.Second
}
return RepositoryValidator{
allowedTargets: allowedTargets,
allowImageRendering: allowImageRendering,
minSyncInterval: minSyncInterval,
}
}
// ValidateRepository solely does configuration checks on the repository object. It does not run a health check or compare against existing repositories.
// isCreate indicates whether this is a CREATE operation (true) or UPDATE operation (false).
// When isCreate is false, allowedTargets validation is skipped to allow existing repositories to continue working.
func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool) field.ErrorList {
list := repo.Validate()
cfg := repo.Config()
if cfg.Spec.Title == "" {
list = append(list, field.Required(field.NewPath("spec", "title"), "a repository title must be given"))
}
if cfg.Spec.Sync.Enabled {
if cfg.Spec.Sync.Target == "" {
list = append(list, field.Required(field.NewPath("spec", "sync", "target"),
"The target type is required when sync is enabled"))
} else if isCreate && !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) {
list = append(list,
field.Invalid(
field.NewPath("spec", "target"),
cfg.Spec.Sync.Target,
"sync target is not supported"))
}
if cfg.Spec.Sync.IntervalSeconds < int64(v.minSyncInterval.Seconds()) {
list = append(list, field.Invalid(field.NewPath("spec", "sync", "intervalSeconds"),
cfg.Spec.Sync.IntervalSeconds, fmt.Sprintf("Interval must be at least %d seconds", int64(v.minSyncInterval.Seconds()))))
}
}
// Reserved names (for now)
reserved := []string{"classic", "sql", "SQL", "plugins", "legacy", "new", "job", "github", "s3", "gcs", "file", "new", "create", "update", "delete"}
if slices.Contains(reserved, cfg.Name) {
list = append(list, field.Invalid(field.NewPath("metadata", "name"), cfg.Name, "Name is reserved, choose a different identifier"))
}
if cfg.Spec.Type != provisioning.LocalRepositoryType && cfg.Spec.Local != nil {
list = append(list, field.Invalid(field.NewPath("spec", "local"),
cfg.Spec.GitHub, "Local config only valid when type is local"))
}
if cfg.Spec.Type != provisioning.GitHubRepositoryType && cfg.Spec.GitHub != nil {
list = append(list, field.Invalid(field.NewPath("spec", "github"),
cfg.Spec.GitHub, "Github config only valid when type is github"))
}
if cfg.Spec.Type != provisioning.GitRepositoryType && cfg.Spec.Git != nil {
list = append(list, field.Invalid(field.NewPath("spec", "git"),
cfg.Spec.Git, "Git config only valid when type is git"))
}
for _, w := range cfg.Spec.Workflows {
switch w {
case provisioning.WriteWorkflow: // valid; no fall thru
case provisioning.BranchWorkflow:
if !cfg.Spec.Type.IsGit() {
list = append(list, field.Invalid(field.NewPath("spec", "workflow"), w, "branch is only supported on git repositories"))
}
default:
list = append(list, field.Invalid(field.NewPath("spec", "workflow"), w, "invalid workflow"))
}
}
if slices.Contains(cfg.Finalizers, RemoveOrphanResourcesFinalizer) &&
slices.Contains(cfg.Finalizers, ReleaseOrphanResourcesFinalizer) {
list = append(list,
field.Invalid(
field.NewPath("medatada", "finalizers"),
cfg.Finalizers,
"cannot have both remove and release orphan resources finalizers",
),
)
}
for _, f := range cfg.Finalizers {
if !slices.Contains(SupportedFinalizers, f) {
list = append(list,
field.Invalid(
field.NewPath("medatada", "finalizers"),
cfg.Finalizers,
fmt.Sprintf("unknown finalizer: %s", f),
),
)
}
}
if !v.allowImageRendering && cfg.Spec.GitHub != nil && cfg.Spec.GitHub.GenerateDashboardPreviews {
list = append(list,
field.Invalid(field.NewPath("spec", "generateDashboardPreviews"),
cfg.Spec.GitHub.GenerateDashboardPreviews,
"image rendering is not enabled"))
}
return list
}
func FromFieldError(err *field.Error) *provisioning.TestResults {
return &provisioning.TestResults{
Code: http.StatusBadRequest,
Success: false,
Errors: []provisioning.ErrorDetails{{
Type: metav1.CauseType(err.Type),
Field: err.Field,
Detail: err.Detail,
}},
}
}