POC/Provisioning: Split each implementation into an interface (#96638)
This commit is contained in:
1 parent
ce473e7d37
commit
d6bd224040
7 files changed
+493
-152
No files matched your search
@@ -0,0 +1,135 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v66/github"
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type githubRepository struct {
|
||||
config *provisioning.Repository
|
||||
}
|
||||
|
||||
func newGithubRepository(config *provisioning.Repository) *githubRepository {
|
||||
return &githubRepository{config}
|
||||
}
|
||||
|
||||
func (r *githubRepository) Config() *provisioning.Repository {
|
||||
return r.config
|
||||
}
|
||||
|
||||
// Validate implements provisioning.Repository.
|
||||
func (r *githubRepository) Validate() (list field.ErrorList) {
|
||||
gh := r.config.Spec.GitHub
|
||||
if gh == nil {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required"))
|
||||
return list
|
||||
}
|
||||
if gh.Owner == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "owner"), "a github repo owner is required"))
|
||||
}
|
||||
if gh.Repository == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "repository"), "a github repo name is required"))
|
||||
}
|
||||
if gh.Token == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required"))
|
||||
}
|
||||
if gh.GenerateDashboardPreviews && !gh.BranchWorkflow {
|
||||
list = append(list, field.Forbidden(field.NewPath("spec", "github", "token"), "to generate dashboard previews, you must activate the branch workflow"))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Test implements provisioning.Repository.
|
||||
func (r *githubRepository) Test(ctx context.Context) error {
|
||||
return &errors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "test is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add validation in admission hook
|
||||
// extractOwnerAndRepo takes a GitHub repository URL and returns the owner and repo name.
|
||||
func extractOwnerAndRepo(repoURL string) (string, string, error) {
|
||||
parsedURL, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
// Split the path to get owner and repo
|
||||
parts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("URL does not contain owner and repo")
|
||||
}
|
||||
|
||||
owner := parts[0]
|
||||
repo := parts[1]
|
||||
return owner, repo, nil
|
||||
}
|
||||
|
||||
// ReadResource implements provisioning.Repository.
|
||||
func (r *githubRepository) ReadResource(ctx context.Context, filePath string, commit string) (*provisioning.ResourceWrapper, error) {
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: r.config.Spec.GitHub.Token},
|
||||
)
|
||||
tokenClient := oauth2.NewClient(ctx, tokenSrc)
|
||||
githubClient := github.NewClient(tokenClient)
|
||||
|
||||
owner, repoName, err := extractOwnerAndRepo(r.config.Spec.GitHub.Repository)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract owner and repo: %w", err)
|
||||
}
|
||||
|
||||
content, _, _, err := githubClient.Repositories.GetContents(ctx, owner, repoName, filePath, &github.RepositoryContentGetOptions{
|
||||
Ref: commit,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: return bad request status code
|
||||
return nil, fmt.Errorf("failed to get content: %w", err)
|
||||
}
|
||||
|
||||
data, err := content.GetContent()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dashboardJSON map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(data), &dashboardJSON); err != nil {
|
||||
// TODO: return bad request status code
|
||||
return nil, fmt.Errorf("failed to unmarshal content: %w", err)
|
||||
}
|
||||
|
||||
// TODO: validate the object is a valid dashboard
|
||||
// Return the wrapped response
|
||||
// Note we can not return it directly (using responder) because that will make sure the
|
||||
// top level apiVersion is provisioning, not the remote resource
|
||||
return &provisioning.ResourceWrapper{
|
||||
Commit: commit,
|
||||
Resource: common.Unstructured{
|
||||
Object: dashboardJSON,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *githubRepository) Webhook() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// really any handler
|
||||
// get the repository from the path
|
||||
_, _ = w.Write([]byte("TODO... handle github webhook " + r.URL.Path))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type LocalFolderResolver struct {
|
||||
// Local path to data directory
|
||||
ProvisioningPath string
|
||||
}
|
||||
|
||||
func (r *LocalFolderResolver) LocalPath(path string) string {
|
||||
// Ignore anything with funky path names
|
||||
if strings.Contains(path, "..") {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.SplitN(path, "/", 1)
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
switch parts[0] {
|
||||
case "provisioning":
|
||||
if r.ProvisioningPath == "" {
|
||||
return "" // not allowed
|
||||
}
|
||||
return filepath.Join(r.ProvisioningPath, parts[1])
|
||||
|
||||
// Can we support pointing to files in devenv
|
||||
case "devenv":
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var _ Repository = (*localRepository)(nil)
|
||||
|
||||
type localRepository struct {
|
||||
config *provisioning.Repository
|
||||
|
||||
// validated path that can be read if not empty
|
||||
path string
|
||||
}
|
||||
|
||||
func newLocalRepository(config *provisioning.Repository, resolver *LocalFolderResolver) *localRepository {
|
||||
r := &localRepository{
|
||||
config: config,
|
||||
}
|
||||
if config.Spec.Local != nil {
|
||||
r.path = resolver.LocalPath(config.Spec.Local.Path)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *localRepository) Config() *provisioning.Repository {
|
||||
return r.config
|
||||
}
|
||||
|
||||
// Validate implements provisioning.Repository.
|
||||
func (r *localRepository) Validate() (fields field.ErrorList) {
|
||||
if r.config.Spec.Type != provisioning.LocalRepositoryType {
|
||||
fields = append(fields, &field.Error{
|
||||
Type: field.ErrorTypeInvalid,
|
||||
Field: "spec.type",
|
||||
Detail: "Local repository requires spec.type=local",
|
||||
})
|
||||
}
|
||||
|
||||
cfg := r.config.Spec.Local
|
||||
if cfg == nil {
|
||||
fields = append(fields, &field.Error{
|
||||
Type: field.ErrorTypeRequired,
|
||||
Field: "spec.local",
|
||||
})
|
||||
return fields
|
||||
}
|
||||
|
||||
// The path value must be set for local provisioning
|
||||
if cfg.Path == "" {
|
||||
fields = append(fields, field.Required(field.NewPath("spec", "local", "path"),
|
||||
"must enter a path to local file"))
|
||||
} else if r.path == "" {
|
||||
fields = append(fields, field.Invalid(field.NewPath("spec", "local", "path"),
|
||||
cfg.Path, "configured path is not allowed, see system allow list"))
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// Test implements provisioning.Repository.
|
||||
func (r *localRepository) Test(ctx context.Context) error {
|
||||
return &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "test is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReadResource implements provisioning.Repository.
|
||||
func (r *localRepository) ReadResource(ctx context.Context, path string, commit string) (*provisioning.ResourceWrapper, error) {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "read resource is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *localRepository) Webhook() http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
return nil
|
||||
}
|
||||
@@ -2,26 +2,18 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/google/go-github/v66/github"
|
||||
)
|
||||
|
||||
type readConnector struct {
|
||||
getter rest.Getter
|
||||
getter RepoGetter
|
||||
}
|
||||
|
||||
func (*readConnector) New() runtime.Object {
|
||||
@@ -56,70 +48,26 @@ func (*readConnector) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
}
|
||||
|
||||
func (s *readConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
obj, err := s.getter.Get(ctx, name, &metav1.GetOptions{})
|
||||
repo, err := s.getter.GetRepository(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected repository, but got %t", obj)
|
||||
}
|
||||
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: repo.Spec.GitHub.Token},
|
||||
)
|
||||
tokenClient := oauth2.NewClient(ctx, tokenSrc)
|
||||
githubClient := github.NewClient(tokenClient)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
idx := strings.Index(r.URL.Path, "/"+name+"/read")
|
||||
filePath := strings.TrimLeft(r.URL.Path[idx+len(name+"/read")+2:], "/")
|
||||
if filePath == "" {
|
||||
// TODO: return bad request status code
|
||||
responder.Error(fmt.Errorf("missing path"))
|
||||
responder.Error(errors.NewBadRequest("missing path"))
|
||||
return
|
||||
}
|
||||
commit := r.URL.Query().Get("commit")
|
||||
|
||||
owner, repoName, err := extractOwnerAndRepo(repo.Spec.GitHub.Repository)
|
||||
if err != nil {
|
||||
responder.Error(fmt.Errorf("failed to extract owner and repo: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
content, _, _, err := githubClient.Repositories.GetContents(ctx, owner, repoName, filePath, &github.RepositoryContentGetOptions{
|
||||
Ref: commit,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: return bad request status code
|
||||
responder.Error(fmt.Errorf("failed to get content: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := content.GetContent()
|
||||
rsp, err := repo.ReadResource(ctx, filePath, commit)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
} else {
|
||||
responder.Object(200, rsp)
|
||||
}
|
||||
|
||||
var dashboardJSON map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(data), &dashboardJSON); err != nil {
|
||||
// TODO: return bad request status code
|
||||
responder.Error(fmt.Errorf("failed to unmarshal content: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: validate the object is a valid dashboard
|
||||
// Return the wrapped response
|
||||
// Note we can not return it directly (using responder) because that will make sure the
|
||||
// top level apiVersion is provisioning, not the remote resource
|
||||
wrapper := &provisioning.ResourceWrapper{
|
||||
Commit: commit,
|
||||
Resource: common.Unstructured{
|
||||
Object: dashboardJSON,
|
||||
},
|
||||
}
|
||||
responder.Object(http.StatusOK, wrapper)
|
||||
}), nil
|
||||
}
|
||||
|
||||
@@ -130,22 +78,3 @@ var (
|
||||
_ rest.SingularNameProvider = (*readConnector)(nil)
|
||||
_ rest.StorageMetadata = (*readConnector)(nil)
|
||||
)
|
||||
|
||||
// TODO: add validation in admission hook
|
||||
// extractOwnerAndRepo takes a GitHub repository URL and returns the owner and repo name.
|
||||
func extractOwnerAndRepo(repoURL string) (string, string, error) {
|
||||
parsedURL, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
// Split the path to get owner and repo
|
||||
parts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("URL does not contain owner and repo")
|
||||
}
|
||||
|
||||
owner := parts[0]
|
||||
repo := parts[1]
|
||||
return owner, repo, nil
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -23,26 +22,38 @@ import (
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var _ builder.APIGroupBuilder = (*ProvisioningAPIBuilder)(nil)
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*ProvisioningAPIBuilder)(nil)
|
||||
_ RepoGetter = (*ProvisioningAPIBuilder)(nil)
|
||||
)
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type ProvisioningAPIBuilder struct{}
|
||||
type ProvisioningAPIBuilder struct {
|
||||
getter rest.Getter
|
||||
localFileResolver *LocalFolderResolver
|
||||
}
|
||||
|
||||
func NewProvisioningAPIBuilder() *ProvisioningAPIBuilder {
|
||||
return &ProvisioningAPIBuilder{}
|
||||
func NewProvisioningAPIBuilder(local *LocalFolderResolver) *ProvisioningAPIBuilder {
|
||||
return &ProvisioningAPIBuilder{
|
||||
localFileResolver: local,
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
reg prometheus.Registerer,
|
||||
cfg *setting.Cfg,
|
||||
) *ProvisioningAPIBuilder {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
builder := NewProvisioningAPIBuilder()
|
||||
builder := NewProvisioningAPIBuilder(&LocalFolderResolver{
|
||||
ProvisioningPath: cfg.ProvisioningPath,
|
||||
})
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
@@ -86,6 +97,7 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
|
||||
return fmt.Errorf("failed to create repository storage: %w", err)
|
||||
}
|
||||
repositoryStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, repositoryStorage)
|
||||
b.getter = repositoryStorage
|
||||
|
||||
helloWorld := &helloWorldSubresource{
|
||||
getter: repositoryStorage,
|
||||
@@ -98,79 +110,74 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("hello")] = helloWorld
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("webhook")] = &webhookConnector{
|
||||
getter: repositoryStorage,
|
||||
getter: b,
|
||||
}
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("read")] = &readConnector{
|
||||
getter: repositoryStorage,
|
||||
getter: b,
|
||||
}
|
||||
apiGroupInfo.VersionedResourcesStorageMap[provisioning.VERSION] = storage
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
obj := a.GetObject()
|
||||
func (b *ProvisioningAPIBuilder) GetRepository(ctx context.Context, name string) (Repository, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *ProvisioningAPIBuilder) getRepository(ctx context.Context, obj runtime.Object) (Repository, error) {
|
||||
if obj == nil {
|
||||
return fmt.Errorf("missing object for validation")
|
||||
return nil, fmt.Errorf("missing object for validation")
|
||||
}
|
||||
r, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected repository configuration")
|
||||
return nil, fmt.Errorf("expected repository configuration")
|
||||
}
|
||||
|
||||
var list field.ErrorList
|
||||
if r.Spec.Title == "" {
|
||||
switch r.Spec.Type {
|
||||
case provisioning.LocalRepositoryType:
|
||||
return newLocalRepository(r, b.localFileResolver), nil
|
||||
case provisioning.GithubRepositoryType:
|
||||
return newGithubRepository(r), nil
|
||||
case provisioning.S3RepositoryType:
|
||||
return newS3Repository(r), nil
|
||||
default:
|
||||
}
|
||||
return &unknownRepository{config: r}, nil
|
||||
}
|
||||
|
||||
func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
repo, err := b.getRepository(ctx, a.GetObject())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Typed validation
|
||||
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"))
|
||||
}
|
||||
|
||||
// Reserved names (for now)
|
||||
reserved := []string{"classic", "SQL", "plugins", "legacy"}
|
||||
if slices.Contains(reserved, r.Name) {
|
||||
list = append(list, field.Invalid(field.NewPath("metadata", "name"), r.Name, "Name is reserved"))
|
||||
if slices.Contains(reserved, cfg.Name) {
|
||||
list = append(list, field.Invalid(field.NewPath("metadata", "name"), cfg.Name, "Name is reserved"))
|
||||
}
|
||||
|
||||
switch r.Spec.Type {
|
||||
case provisioning.LocalRepositoryType:
|
||||
if r.Spec.Local == nil || r.Spec.Local.Path == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "local", "path"), "a path to a local file system is required"))
|
||||
} else {
|
||||
// TODO... configure an allow list of paths we can read
|
||||
if !strings.HasPrefix(r.Spec.Local.Path, "/tmp/") {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", "local", "path"), r.Spec.Local.Path,
|
||||
"invalid local file for provisioning"))
|
||||
}
|
||||
}
|
||||
case provisioning.S3RepositoryType:
|
||||
s3 := r.Spec.S3
|
||||
if s3 == nil {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3"), "an s3 config is required"))
|
||||
break
|
||||
}
|
||||
if s3.Region == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3", "region"), "an s3 region is required"))
|
||||
}
|
||||
if s3.Bucket == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3", "bucket"), "an s3 bucket name is required"))
|
||||
}
|
||||
case provisioning.GithubRepositoryType:
|
||||
gh := r.Spec.GitHub
|
||||
if gh == nil {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required"))
|
||||
break
|
||||
}
|
||||
if gh.Owner == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "owner"), "a github repo owner is required"))
|
||||
}
|
||||
if gh.Repository == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "repository"), "a github repo name is required"))
|
||||
}
|
||||
if gh.Token == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required"))
|
||||
}
|
||||
if gh.GenerateDashboardPreviews && !gh.BranchWorkflow {
|
||||
list = append(list, field.Forbidden(field.NewPath("spec", "github", "token"), "to generate dashboard previews, you must activate the branch workflow"))
|
||||
}
|
||||
default:
|
||||
list = append(list, field.TypeInvalid(field.NewPath("spec", "type"), r.Spec.Type, "the repository type must be one of local, s3, or github"))
|
||||
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.LocalRepositoryType && 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.LocalRepositoryType && cfg.Spec.S3 != nil {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", "s3"),
|
||||
cfg.Spec.GitHub, "S3 config only valid when type is s3"))
|
||||
}
|
||||
|
||||
if len(list) > 0 {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type s3Repository struct {
|
||||
config *provisioning.Repository
|
||||
}
|
||||
|
||||
func newS3Repository(config *provisioning.Repository) *s3Repository {
|
||||
return &s3Repository{config}
|
||||
}
|
||||
|
||||
func (r *s3Repository) Config() *provisioning.Repository {
|
||||
return r.config
|
||||
}
|
||||
|
||||
// Validate implements provisioning.Repository.
|
||||
func (r *s3Repository) Validate() (list field.ErrorList) {
|
||||
s3 := r.Config().Spec.S3
|
||||
if s3 == nil {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3"), "an s3 config is required"))
|
||||
return
|
||||
}
|
||||
if s3.Region == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3", "region"), "an s3 region is required"))
|
||||
}
|
||||
if s3.Bucket == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "s3", "bucket"), "an s3 bucket name is required"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Test implements provisioning.Repository.
|
||||
func (r *s3Repository) Test(ctx context.Context) error {
|
||||
return &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "test is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReadResource implements provisioning.Repository.
|
||||
func (r *s3Repository) ReadResource(ctx context.Context, path string, commit string) (*provisioning.ResourceWrapper, error) {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "read resource is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *s3Repository) Webhook() http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type RepoGetter interface {
|
||||
GetRepository(ctx context.Context, name string) (Repository, error)
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
// The saved object
|
||||
Config() *provisioning.Repository
|
||||
|
||||
// This is called before trying to create/update a saved resource
|
||||
// This is not an indication that the connection information works,
|
||||
// just that they are reasonably configured
|
||||
Validate() field.ErrorList
|
||||
|
||||
// Called to check if all connection information actually works
|
||||
Test(ctx context.Context) error
|
||||
|
||||
// Read a resource from settings
|
||||
ReadResource(ctx context.Context, path string, commit string) (*provisioning.ResourceWrapper, error)
|
||||
|
||||
// For repositories that support webhooks
|
||||
Webhook() http.HandlerFunc
|
||||
}
|
||||
|
||||
var _ Repository = (*unknownRepository)(nil)
|
||||
|
||||
type unknownRepository struct {
|
||||
config *provisioning.Repository
|
||||
}
|
||||
|
||||
func (r *unknownRepository) Config() *provisioning.Repository {
|
||||
return r.config
|
||||
}
|
||||
|
||||
// Validate implements provisioning.Repository.
|
||||
func (r *unknownRepository) Validate() (fields field.ErrorList) {
|
||||
if r.config.Spec.Type == "" {
|
||||
return field.ErrorList{
|
||||
field.Required(field.NewPath("spec", "type"), "must specify a repository type"),
|
||||
}
|
||||
}
|
||||
return field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "type"), r.config.Spec.Type, "unsupported repository type"),
|
||||
}
|
||||
}
|
||||
|
||||
// Test implements provisioning.Repository.
|
||||
func (r *unknownRepository) Test(ctx context.Context) error {
|
||||
return &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "test is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReadResource implements provisioning.Repository.
|
||||
func (r *unknownRepository) ReadResource(ctx context.Context, path string, commit string) (*provisioning.ResourceWrapper, error) {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: "read resource is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *unknownRepository) Webhook() http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
return nil
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
|
||||
// This only works for github right now
|
||||
type webhookConnector struct {
|
||||
getter rest.Getter
|
||||
getter RepoGetter
|
||||
}
|
||||
|
||||
func (*webhookConnector) New() runtime.Object {
|
||||
@@ -52,23 +53,20 @@ func (*webhookConnector) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
}
|
||||
|
||||
func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
obj, err := s.getter.Get(ctx, name, &metav1.GetOptions{})
|
||||
repo, err := s.getter.GetRepository(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected repository, but got %t", obj)
|
||||
webhook := repo.Webhook()
|
||||
if webhook == nil {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: v1.Status{
|
||||
Message: fmt.Sprintf("webhook is not implemented for: %s", repo.Config().Spec.Type),
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
if repo.Spec.Type != provisioning.GithubRepositoryType {
|
||||
return nil, fmt.Errorf("only works for github")
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// really any handler
|
||||
// get the repository from the path
|
||||
_, _ = w.Write([]byte("TODO... handle webhook " + r.URL.Path))
|
||||
}), nil
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Reference in new issue
Block a user