POC/Provisioning: Split each implementation into an interface (#96638)

This commit is contained in:
Ryan McKinley authored and GitHub committed 2024-11-18 18:28:39 +03:00
1 parent ce473e7d37
commit d6bd224040
7 files changed
+493 -152

No files matched your search

+135
View File
@@ -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))
}
}
+123
View File
@@ -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
}
+7 -78
View File
@@ -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
}
+66 -59
View File
@@ -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 {
+66
View File
@@ -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
}
+83
View File
@@ -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
}
+13 -15
View File
@@ -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 (