package repository import ( "context" "fmt" "net/http" "slices" 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" ) // RepositoryValidator interface for validating repositories against existing ones type RepositoryValidator interface { VerifyAgainstExistingRepositories(ctx context.Context, cfg *provisioning.Repository) *field.Error } func TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { return TestRepositoryWithValidator(ctx, repo, nil) } func TestRepositoryWithValidator(ctx context.Context, repo Repository, validator RepositoryValidator) (*provisioning.TestResults, error) { errors := ValidateRepository(repo) if len(errors) > 0 { rsp := &provisioning.TestResults{ Code: http.StatusUnprocessableEntity, // Invalid Success: false, Errors: make([]provisioning.ErrorDetails, len(errors)), } for i, err := range errors { rsp.Errors[i] = provisioning.ErrorDetails{ Type: metav1.CauseType(err.Type), Field: err.Field, Detail: err.Detail, } } return rsp, nil } rsp, err := repo.Test(ctx) if err != nil { return nil, err } if rsp.Success && validator != nil { cfg := repo.Config() if validationErr := validator.VerifyAgainstExistingRepositories(ctx, cfg); validationErr != nil { rsp = &provisioning.TestResults{ Success: false, Code: http.StatusUnprocessableEntity, Errors: []provisioning.ErrorDetails{{ Type: metav1.CauseType(validationErr.Type), Field: validationErr.Field, Detail: validationErr.Detail, }}, } } } return rsp, nil } func ValidateRepository(repo Repository) 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 && cfg.Spec.Sync.Target == "" { list = append(list, field.Required(field.NewPath("spec", "sync", "target"), "The target type is required when sync is enabled")) } // 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), ), ) } } 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, }}, } }