diff --git a/.betterer.results b/.betterer.results
index cdb1c49806f..c9e8cfc6352 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -5731,8 +5731,8 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "22"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "23"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "24"],
- [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "25"],
- [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "26"]
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "25"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "26"]
],
"public/app/features/provisioning/DeleteRepositoryButton.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"],
diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go
index ab1417b9980..064aab7242c 100644
--- a/pkg/apis/provisioning/v0alpha1/types.go
+++ b/pkg/apis/provisioning/v0alpha1/types.go
@@ -28,10 +28,10 @@ type LocalRepositoryConfig struct {
type Workflow string
const (
- // BranchWorkflow creates a branch for changes
+ // WriteWorkflow allows a user to write directly to the repository
+ WriteWorkflow Workflow = "write"
+ // BranchWorkflow allows creating a branch for changes (where we can then make a PR)
BranchWorkflow Workflow = "branch"
- // PushWorkflow pushes changes directly the configured branch
- PushWorkflow Workflow = "push"
)
type GitHubRepositoryConfig struct {
@@ -39,19 +39,14 @@ type GitHubRepositoryConfig struct {
URL string `json:"url,omitempty"`
// The branch to use in the repository.
- // By default, this is the main branch.
- Branch string `json:"branch,omitempty"`
+ Branch string `json:"branch"`
+
// Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.
Token string `json:"token,omitempty"`
// Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.
// +listType=atomic
EncryptedToken []byte `json:"encryptedToken,omitempty"`
- // Workflow allowed for changes to the repository.
- // The order is relevant for defining the precedence of the workflows.
- // Possible values: pull-request, branch, push.
- Workflows []Workflow `json:"workflows,omitempty"`
-
// Whether we should show dashboard previews for pull requests.
// By default, this is false (i.e. we will not create previews).
GenerateDashboardPreviews bool `json:"generateDashboardPreviews,omitempty"`
@@ -74,8 +69,10 @@ type RepositorySpec struct {
// Repository description
Description string `json:"description,omitempty"`
- // ReadOnly repository does not allow any write commands
- ReadOnly bool `json:"readOnly"`
+ // UI driven Workflow that allow changes to the contends of the repository.
+ // The order is relevant for defining the precedence of the workflows.
+ // When empty, the repository does not support any edits (eg, readonly)
+ Workflows []Workflow `json:"workflows"`
// Sync settings -- how values are pulled from the repository into grafana
Sync SyncOptions `json:"sync"`
diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go
index 6dc88372968..91db1c201ee 100644
--- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go
@@ -98,11 +98,6 @@ func (in *GitHubRepositoryConfig) DeepCopyInto(out *GitHubRepositoryConfig) {
*out = make([]byte, len(*in))
copy(*out, *in)
}
- if in.Workflows != nil {
- in, out := &in.Workflows, &out.Workflows
- *out = make([]Workflow, len(*in))
- copy(*out, *in)
- }
return
}
@@ -469,6 +464,11 @@ func (in *RepositoryList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) {
*out = *in
+ if in.Workflows != nil {
+ in, out := &in.Workflows, &out.Workflows
+ *out = make([]Workflow, len(*in))
+ copy(*out, *in)
+ }
out.Sync = in.Sync
if in.Local != nil {
in, out := &in.Local, &out.Local
diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go
index 7fa855a69bc..4c14b254414 100644
--- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go
+++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go
@@ -240,7 +240,8 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
},
"branch": {
SchemaProps: spec.SchemaProps{
- Description: "The branch to use in the repository. By default, this is the main branch.",
+ Description: "The branch to use in the repository.",
+ Default: "",
Type: []string{"string"},
Format: "",
},
@@ -264,22 +265,6 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
Format: "byte",
},
},
- "workflows": {
- SchemaProps: spec.SchemaProps{
- Description: "Workflow allowed for changes to the repository. The order is relevant for defining the precedence of the workflows. Possible values: pull-request, branch, push.",
- Type: []string{"array"},
- Items: &spec.SchemaOrArray{
- Schema: &spec.Schema{
- SchemaProps: spec.SchemaProps{
- Default: "",
- Type: []string{"string"},
- Format: "",
- Enum: []interface{}{"branch", "push"},
- },
- },
- },
- },
- },
"generateDashboardPreviews": {
SchemaProps: spec.SchemaProps{
Description: "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).",
@@ -288,6 +273,7 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
},
},
},
+ Required: []string{"branch"},
},
},
}
@@ -1004,12 +990,20 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCa
Format: "",
},
},
- "readOnly": {
+ "workflows": {
SchemaProps: spec.SchemaProps{
- Description: "ReadOnly repository does not allow any write commands",
- Default: false,
- Type: []string{"boolean"},
- Format: "",
+ Description: "UI driven Workflow taht allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly)",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ Enum: []interface{}{"branch", "write"},
+ },
+ },
+ },
},
},
"sync": {
@@ -1041,7 +1035,7 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCa
},
},
},
- Required: []string{"title", "readOnly", "sync", "type"},
+ Required: []string{"title", "workflows", "sync", "type"},
},
},
Dependencies: []string{
diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list
index 313713c794b..696b361ec80 100644
--- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list
+++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list
@@ -1,10 +1,10 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,FileList,Items
-API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,GitHubRepositoryConfig,Workflows
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,HistoryList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Errors
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Summary
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryList,Items
+API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryViewList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceStats,Items
diff --git a/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go b/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go
index 3003228ef15..e3cc1f0829c 100644
--- a/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go
+++ b/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubrepositoryconfig.go
@@ -4,19 +4,14 @@
package v0alpha1
-import (
- provisioningv0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
-)
-
// GitHubRepositoryConfigApplyConfiguration represents a declarative configuration of the GitHubRepositoryConfig type for use
// with apply.
type GitHubRepositoryConfigApplyConfiguration struct {
- URL *string `json:"url,omitempty"`
- Branch *string `json:"branch,omitempty"`
- Token *string `json:"token,omitempty"`
- EncryptedToken []byte `json:"encryptedToken,omitempty"`
- Workflows []provisioningv0alpha1.Workflow `json:"workflows,omitempty"`
- GenerateDashboardPreviews *bool `json:"generateDashboardPreviews,omitempty"`
+ URL *string `json:"url,omitempty"`
+ Branch *string `json:"branch,omitempty"`
+ Token *string `json:"token,omitempty"`
+ EncryptedToken []byte `json:"encryptedToken,omitempty"`
+ GenerateDashboardPreviews *bool `json:"generateDashboardPreviews,omitempty"`
}
// GitHubRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitHubRepositoryConfig type for use with
@@ -59,16 +54,6 @@ func (b *GitHubRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...
return b
}
-// WithWorkflows adds the given value to the Workflows field in the declarative configuration
-// and returns the receiver, so that objects can be build by chaining "With" function invocations.
-// If called multiple times, values provided by each call will be appended to the Workflows field.
-func (b *GitHubRepositoryConfigApplyConfiguration) WithWorkflows(values ...provisioningv0alpha1.Workflow) *GitHubRepositoryConfigApplyConfiguration {
- for i := range values {
- b.Workflows = append(b.Workflows, values[i])
- }
- return b
-}
-
// WithGenerateDashboardPreviews sets the GenerateDashboardPreviews field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the GenerateDashboardPreviews field is set to the value of the last call.
diff --git a/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go b/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go
index b0d4769286d..75508c2003b 100644
--- a/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go
+++ b/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go
@@ -13,7 +13,7 @@ import (
type RepositorySpecApplyConfiguration struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
- ReadOnly *bool `json:"readOnly,omitempty"`
+ Workflows []provisioningv0alpha1.Workflow `json:"workflows,omitempty"`
Sync *SyncOptionsApplyConfiguration `json:"sync,omitempty"`
Type *provisioningv0alpha1.RepositoryType `json:"type,omitempty"`
Local *LocalRepositoryConfigApplyConfiguration `json:"local,omitempty"`
@@ -42,11 +42,13 @@ func (b *RepositorySpecApplyConfiguration) WithDescription(value string) *Reposi
return b
}
-// WithReadOnly sets the ReadOnly field in the declarative configuration to the given value
-// and returns the receiver, so that objects can be built by chaining "With" function invocations.
-// If called multiple times, the ReadOnly field is set to the value of the last call.
-func (b *RepositorySpecApplyConfiguration) WithReadOnly(value bool) *RepositorySpecApplyConfiguration {
- b.ReadOnly = &value
+// WithWorkflows adds the given value to the Workflows field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Workflows field.
+func (b *RepositorySpecApplyConfiguration) WithWorkflows(values ...provisioningv0alpha1.Workflow) *RepositorySpecApplyConfiguration {
+ for i := range values {
+ b.Workflows = append(b.Workflows, values[i])
+ }
return b
}
diff --git a/pkg/registry/apis/provisioning/export.go b/pkg/registry/apis/provisioning/export.go
index 29858a181ed..df108b0eedb 100644
--- a/pkg/registry/apis/provisioning/export.go
+++ b/pkg/registry/apis/provisioning/export.go
@@ -12,6 +12,7 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
+ "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
type exportConnector struct {
@@ -52,11 +53,8 @@ func (c *exportConnector) Connect(
return nil, err
}
cfg := repo.Config()
- if cfg.Spec.ReadOnly {
- return nil, &apierrors.StatusError{ErrStatus: v1.Status{
- Code: http.StatusPreconditionFailed,
- Message: "Repository is read only",
- }}
+ if err := repository.IsWriteAllowed(cfg, ""); err != nil {
+ return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go
index 2ac7eb90746..af07496f897 100644
--- a/pkg/registry/apis/provisioning/files.go
+++ b/pkg/registry/apis/provisioning/files.go
@@ -186,9 +186,8 @@ func (s *filesConnector) doRead(ctx context.Context, repo repository.Repository,
}
func (s *filesConnector) doWrite(ctx context.Context, update bool, repo repository.Repository, path string, ref string, message string, req *http.Request) (*provisioning.ResourceWrapper, error) {
- if repo.Config().Spec.ReadOnly {
- return nil, apierrors.NewForbidden(provisioning.RepositoryResourceInfo.GroupResource(),
- "this repository is read only", nil)
+ if err := repository.IsWriteAllowed(repo.Config(), ref); err != nil {
+ return nil, err
}
defer func() { _ = req.Body.Close() }()
@@ -276,9 +275,8 @@ func (s *filesConnector) doWrite(ctx context.Context, update bool, repo reposito
}
func (s *filesConnector) doDelete(ctx context.Context, repo repository.Repository, path string, ref string, message string) (*provisioning.ResourceWrapper, error) {
- if repo.Config().Spec.ReadOnly {
- return nil, apierrors.NewForbidden(provisioning.RepositoryResourceInfo.GroupResource(),
- "this repository is read only", nil)
+ if err := repository.IsWriteAllowed(repo.Config(), ref); err != nil {
+ return nil, err
}
err := repo.Delete(ctx, path, ref, message)
diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go
index 03b4bf976bd..05c7f9c7af1 100644
--- a/pkg/registry/apis/provisioning/jobs/export/worker.go
+++ b/pkg/registry/apis/provisioning/jobs/export/worker.go
@@ -48,15 +48,16 @@ func (r *ExportWorker) IsSupported(ctx context.Context, job provisioning.Job) bo
// Process will start a job
func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error {
- if repo.Config().Spec.ReadOnly {
- return errors.New("read only repository")
- }
-
options := job.Spec.Export
if options == nil {
return errors.New("missing export settings")
}
+ // Can write to external branch
+ if err := repository.IsWriteAllowed(repo.Config(), options.Branch); err != nil {
+ return err
+ }
+
var (
err error
buffered *gogit.GoGitRepo
diff --git a/pkg/registry/apis/provisioning/jobs/migrate/utils.go b/pkg/registry/apis/provisioning/jobs/migrate/utils.go
index 3afcaf58126..f9c42b77ee7 100644
--- a/pkg/registry/apis/provisioning/jobs/migrate/utils.go
+++ b/pkg/registry/apis/provisioning/jobs/migrate/utils.go
@@ -8,7 +8,8 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
-func verifyEmptyRepo(tree []repository.FileTreeEntry) error {
+// VerifyEmptyRepo -- checks if there is anything useable in the tree
+func VerifyEmptyRepo(tree []repository.FileTreeEntry) error {
var folders []string
var files []string
diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go
index 52d0e6267b6..39428a0a050 100644
--- a/pkg/registry/apis/provisioning/register.go
+++ b/pkg/registry/apis/provisioning/register.go
@@ -376,21 +376,14 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis
return fmt.Errorf("github configuration is required")
}
- if r.Spec.GitHub.Branch == "" {
- r.Spec.GitHub.Branch = "main"
- }
-
// Trim trailing slash or .git
if len(r.Spec.GitHub.URL) > 5 {
r.Spec.GitHub.URL = strings.TrimRight(strings.TrimRight(r.Spec.GitHub.URL, "/"), ".git")
}
+ }
- if len(r.Spec.GitHub.Workflows) == 0 {
- r.Spec.GitHub.Workflows = []provisioning.Workflow{
- provisioning.BranchWorkflow,
- provisioning.PushWorkflow,
- }
- }
+ if r.Spec.Workflows == nil {
+ r.Spec.Workflows = []provisioning.Workflow{}
}
if err := b.encryptSecrets(ctx, r); err != nil {
diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go
index 3147e0c9f31..f6a2a5fbb3a 100644
--- a/pkg/registry/apis/provisioning/repository/github.go
+++ b/pkg/registry/apis/provisioning/repository/github.go
@@ -98,14 +98,6 @@ func (r *githubRepository) Validate() (list field.ErrorList) {
list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required"))
}
- for _, w := range gh.Workflows {
- switch w {
- case provisioning.PushWorkflow, provisioning.BranchWorkflow:
- default:
- list = append(list, field.Invalid(field.NewPath("spec", "github", "workflow"), w, "invalid workflow"))
- }
- }
-
return list
}
@@ -262,9 +254,6 @@ func (r *githubRepository) Create(ctx context.Context, path, ref string, data []
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
- if err := r.isWorkflowAllowed(ref); err != nil {
- return err
- }
if err := r.ensureBranchExists(ctx, ref); err != nil {
return fmt.Errorf("create branch on create: %w", err)
@@ -297,9 +286,6 @@ func (r *githubRepository) Update(ctx context.Context, path, ref string, data []
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
- if err := r.isWorkflowAllowed(ref); err != nil {
- return err
- }
if err := r.ensureBranchExists(ctx, ref); err != nil {
return fmt.Errorf("create branch on update: %w", err)
@@ -333,9 +319,6 @@ func (r *githubRepository) Write(ctx context.Context, path string, ref string, d
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
- if err := r.isWorkflowAllowed(ref); err != nil {
- return err
- }
return writeWithReadThenCreateOrUpdate(ctx, r, path, ref, data, message)
}
@@ -345,9 +328,6 @@ func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string
ref = r.config.Spec.GitHub.Branch
}
ctx, _ = r.logger(ctx, ref)
- if err := r.isWorkflowAllowed(ref); err != nil {
- return err
- }
if err := r.ensureBranchExists(ctx, ref); err != nil {
return fmt.Errorf("create branch on delete: %w", err)
@@ -440,25 +420,6 @@ func (r *githubRepository) History(ctx context.Context, path, ref string) ([]pro
return ret, nil
}
-func (r *githubRepository) isWorkflowAllowed(ref string) error {
- cfg := r.config.Spec
-
- workflows := make(map[provisioning.Workflow]bool)
- for _, w := range cfg.GitHub.Workflows {
- workflows[w] = true
- }
-
- if ref == cfg.GitHub.Branch && !workflows[provisioning.PushWorkflow] {
- return apierrors.NewBadRequest("direct push not allowed")
- }
-
- if ref != cfg.GitHub.Branch && !workflows[provisioning.BranchWorkflow] {
- return apierrors.NewBadRequest("branch push not allowed")
- }
-
- return nil
-}
-
// basicGitBranchNameRegex is a regular expression to validate a git branch name
// it does not cover all cases as positive lookaheads are not supported in Go's regexp
var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`)
diff --git a/pkg/registry/apis/provisioning/repository/test.go b/pkg/registry/apis/provisioning/repository/test.go
index 3f5c14c7d0f..8ba064ea08c 100644
--- a/pkg/registry/apis/provisioning/repository/test.go
+++ b/pkg/registry/apis/provisioning/repository/test.go
@@ -6,8 +6,9 @@ import (
"net/http"
"slices"
- provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"k8s.io/apimachinery/pkg/util/validation/field"
+
+ provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
// Tester is a struct that implements the Tester interface
@@ -70,5 +71,17 @@ func ValidateRepository(repo Repository) field.ErrorList {
cfg.Spec.GitHub, "Github config only valid when type is github"))
}
+ for _, w := range cfg.Spec.Workflows {
+ switch w {
+ case provisioning.WriteWorkflow: // valid; no fall thru
+ case provisioning.BranchWorkflow:
+ if cfg.Spec.Type != provisioning.GitHubRepositoryType {
+ 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"))
+ }
+ }
+
return list
}
diff --git a/pkg/registry/apis/provisioning/repository/workflows.go b/pkg/registry/apis/provisioning/repository/workflows.go
new file mode 100644
index 00000000000..2b98f2b438a
--- /dev/null
+++ b/pkg/registry/apis/provisioning/repository/workflows.go
@@ -0,0 +1,40 @@
+package repository
+
+import (
+ "net/http"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
+)
+
+func IsWriteAllowed(repo *provisioning.Repository, ref string) error {
+ if len(repo.Spec.Workflows) == 0 {
+ return apierrors.NewBadRequest("this repository is read only")
+ }
+
+ if ref == "" {
+ for _, v := range repo.Spec.Workflows {
+ if v == provisioning.WriteWorkflow {
+ return nil // found
+ }
+ }
+ return apierrors.NewBadRequest("this repository does not support the write workflow")
+ }
+
+ // Only github
+ if repo.Spec.Type != provisioning.GitHubRepositoryType {
+ return &apierrors.StatusError{ErrStatus: v1.Status{
+ Code: http.StatusPreconditionFailed,
+ Message: "Only github supports writing to a branch",
+ }}
+ }
+
+ for _, v := range repo.Spec.Workflows {
+ if v == provisioning.BranchWorkflow {
+ return nil
+ }
+ }
+ return apierrors.NewBadRequest("this repository does not support the branch workflow")
+}
diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go
index b9a0ddc77c6..d119ead9b98 100644
--- a/pkg/registry/apis/provisioning/routes.go
+++ b/pkg/registry/apis/provisioning/routes.go
@@ -155,7 +155,7 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) {
Name: val.ObjectMeta.Name,
Title: val.Spec.Title,
Type: val.Spec.Type,
- ReadOnly: val.Spec.ReadOnly,
+ ReadOnly: len(val.Spec.Workflows) == 0,
Target: val.Spec.Sync.Target,
}
}
diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json
index cc0ac21790c..362ded043bb 100644
--- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json
+++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json
@@ -2852,10 +2852,14 @@
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.GitHubRepositoryConfig": {
"type": "object",
+ "required": [
+ "branch"
+ ],
"properties": {
"branch": {
- "description": "The branch to use in the repository. By default, this is the main branch.",
- "type": "string"
+ "description": "The branch to use in the repository.",
+ "type": "string",
+ "default": ""
},
"encryptedToken": {
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
@@ -2874,25 +2878,14 @@
"url": {
"description": "The repository URL (e.g. `https://github.com/example/test`).",
"type": "string"
- },
- "workflows": {
- "description": "Workflow allowed for changes to the repository. The order is relevant for defining the precedence of the workflows. Possible values: pull-request, branch, push.",
- "type": "array",
- "items": {
- "type": "string",
- "default": "",
- "enum": [
- "branch",
- "push"
- ]
- }
}
}
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.HealthStatus": {
"type": "object",
"required": [
- "healthy"
+ "healthy",
+ "message"
],
"properties": {
"checked": {
@@ -3396,7 +3389,7 @@
"type": "object",
"required": [
"title",
- "readOnly",
+ "workflows",
"sync",
"type"
],
@@ -3421,11 +3414,6 @@
}
]
},
- "readOnly": {
- "description": "ReadOnly repository does not allow any write commands",
- "type": "boolean",
- "default": false
- },
"sync": {
"description": "Sync settings -- how values are pulled from the repository into grafana",
"default": {},
@@ -3448,6 +3436,18 @@
"github",
"local"
]
+ },
+ "workflows": {
+ "description": "UI driven Workflow taht allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly)",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "default": "",
+ "enum": [
+ "branch",
+ "write"
+ ]
+ }
}
}
},
diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go
index fba70677206..ea2694a1e19 100644
--- a/pkg/tests/apis/provisioning/provisioning_test.go
+++ b/pkg/tests/apis/provisioning/provisioning_test.go
@@ -12,6 +12,8 @@ import (
"testing"
"time"
+ gh "github.com/google/go-github/v69/github"
+ ghmock "github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
@@ -19,7 +21,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
- gh "github.com/google/go-github/v69/github"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
@@ -29,7 +30,6 @@ import (
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
- ghmock "github.com/migueleliasweb/go-github-mock/src/mock"
)
func TestMain(m *testing.M) {
@@ -177,17 +177,26 @@ func TestIntegrationProvisioning(t *testing.T) {
} {
t.Run(inputFilePath, func(t *testing.T) {
input := helper.LoadYAMLOrJSONFile(inputFilePath)
- expectedOutput, err := json.MarshalIndent(input.Object["spec"], "", " ")
- require.NoError(t, err, "failed to marshal JSON from input spec")
- _, err = client.Resource.Create(ctx, input, createOptions)
+ _, err := client.Resource.Create(ctx, input, createOptions)
require.NoError(t, err, "failed to create resource")
output, err := client.Resource.Get(ctx, mustNestedString(input.Object, "metadata", "name"), metav1.GetOptions{})
require.NoError(t, err, "failed to read back resource")
+
+ // Move encrypted token mutation
+ token, found, err := unstructured.NestedString(output.Object, "spec", "github", "encryptedToken")
+ require.NoError(t, err, "encryptedToken is not a string")
+ if found {
+ unstructured.RemoveNestedField(input.Object, "spec", "github", "token")
+ err = unstructured.SetNestedField(input.Object, token, "spec", "github", "encryptedToken")
+ require.NoError(t, err, "unable to copy encrypted token")
+ }
+
+ expectedOutput, err := json.MarshalIndent(input.Object["spec"], "", " ")
+ require.NoError(t, err, "failed to marshal JSON from input spec")
outputJSON, err := json.MarshalIndent(output.Object["spec"], "", " ")
require.NoError(t, err, "failed to marshal JSON from read back resource")
-
require.JSONEq(t, string(expectedOutput), string(outputJSON))
})
}
diff --git a/pkg/tests/apis/provisioning/testdata/github-example.json b/pkg/tests/apis/provisioning/testdata/github-example.json
index 85475eb8613..160db9fbf35 100644
--- a/pkg/tests/apis/provisioning/testdata/github-example.json
+++ b/pkg/tests/apis/provisioning/testdata/github-example.json
@@ -8,7 +8,7 @@
"title": "Github Example",
"description": "load resources from github",
"type": "github",
- "readOnly": false,
+ "workflows": ["branch", "write"],
"sync": {
"enabled": true,
"target": "folder",
@@ -17,7 +17,6 @@
"github": {
"url": "https://github.com/grafana/git-ui-sync-demo",
"branch": "dummy-branch",
- "workflows": ["push", "branch"],
"generateDashboardPreviews": true,
"token": "github_pat_dummy"
}
diff --git a/pkg/tests/apis/provisioning/testdata/local-conf-provisioning-sample.json b/pkg/tests/apis/provisioning/testdata/local-conf-provisioning-sample.json
index cff173b0819..f8d99b52945 100644
--- a/pkg/tests/apis/provisioning/testdata/local-conf-provisioning-sample.json
+++ b/pkg/tests/apis/provisioning/testdata/local-conf-provisioning-sample.json
@@ -11,7 +11,7 @@
},
"title": "Config provisioning files",
"type": "local",
- "readOnly": false,
+ "workflows": ["write"],
"sync": {
"enabled": false,
"target": "folder",
diff --git a/pkg/tests/apis/provisioning/testdata/local-devenv.json b/pkg/tests/apis/provisioning/testdata/local-devenv.json
index 6130961f9c2..6a7dd5e9133 100644
--- a/pkg/tests/apis/provisioning/testdata/local-devenv.json
+++ b/pkg/tests/apis/provisioning/testdata/local-devenv.json
@@ -12,7 +12,7 @@
"target": "folder",
"intervalSeconds": 60
},
- "readOnly": false,
+ "workflows": ["write"],
"type": "local",
"local": {
"path": "devenv/dev-dashboards"
diff --git a/pkg/tests/apis/provisioning/testdata/local-tmp.json b/pkg/tests/apis/provisioning/testdata/local-tmp.json
index 7bb254bba14..6aed89a005e 100644
--- a/pkg/tests/apis/provisioning/testdata/local-tmp.json
+++ b/pkg/tests/apis/provisioning/testdata/local-tmp.json
@@ -14,7 +14,6 @@
"target": "folder",
"intervalSeconds": 60
},
- "readOnly": false,
"title": "Load tmp dashboards",
"type": "local"
}
diff --git a/pkg/tests/apis/provisioning/testdata/local-xxx.json b/pkg/tests/apis/provisioning/testdata/local-xxx.json
index 866d6c73931..a20d30460f6 100644
--- a/pkg/tests/apis/provisioning/testdata/local-xxx.json
+++ b/pkg/tests/apis/provisioning/testdata/local-xxx.json
@@ -9,12 +9,12 @@
"local": {
"path": "conf/provisioning/sample"
},
+ "workflows": [],
"sync": {
"enabled": false,
"target": "folder",
"intervalSeconds": 60
},
- "readOnly": false,
"title": "Config provisioning files",
"type": "local"
}
diff --git a/public/app/features/provisioning/ConfigForm.tsx b/public/app/features/provisioning/ConfigForm.tsx
index db40f170c55..3d87f26d3a0 100644
--- a/public/app/features/provisioning/ConfigForm.tsx
+++ b/public/app/features/provisioning/ConfigForm.tsx
@@ -31,10 +31,16 @@ const targetOptions = [
{ value: 'folder', label: 'Managed folder' },
];
-const workflowOptions: Array> = [
- { label: 'Push', value: 'push' },
- { label: 'Branch', value: 'branch' },
-];
+export function GetWorkflowOptions(type?: 'github' | 'local'): Array> {
+ const opts: Array> = [
+ { label: 'Branch', value: 'branch', description: 'Create a branch (and pull request) for changes' },
+ { label: 'Write', value: 'write', description: 'Allow writing updates to the remore repository' },
+ ];
+ if (type === 'github') {
+ return opts;
+ }
+ return [opts[2]]; // only write
+}
const appEvents = getAppEvents();
@@ -47,13 +53,12 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat
url: '',
branch: 'main',
generateDashboardPreviews: true,
- workflows: ['push', 'branch'],
+ workflows: ['write'], // 'branch' if github
sync: {
enabled: false,
target: 'instance',
intervalSeconds: 60,
},
- readOnly: false,
};
}
return specToData(repository);
@@ -178,17 +183,16 @@ export function ConfigForm({ data }: ConfigFormProps) {
-
- (
-
- )}
- />
-
-
+
+ Render before/after images and link them to the pull request.
+
+ NOTE! these will be public URLs!!!"
+
+ }
+ >
{/* The lint option is intentionally not presented here, as it's an experimental feature. */}
@@ -201,6 +205,23 @@ export function ConfigForm({ data }: ConfigFormProps) {
)}
+
+ (
+
+ )}
+ />
+
+
-