[Prototyping] automatically create github webhook in provisioning (#96645)

This commit is contained in:
Roberto Jiménez Sánchez authored and GitHub committed 2024-11-19 14:54:29 +01:00
1 parent 7a2e9fc0c0
commit 76e494209f
11 files changed
+458 -56

No files matched your search

+8
View File
@@ -37,9 +37,17 @@ type GitHubRepositoryConfig struct {
Owner string `json:"owner,omitempty"`
// The name of the repository (e.g. test in `example/test` or `https://github.com/example/test`).
Repository string `json:"repository,omitempty"`
// The branch to use in the repository.
// By default, this is the main branch.
Branch string `json:"branch,omitempty"`
// Token for accessing the repository.
// TODO: this should be part of secrets and a simple reference.
Token string `json:"token,omitempty"`
// WebhookURL is the URL to send webhooks events to.
// By default, the system will generate a URL for you but you can use this one to run
// grafana locally and test the webhooks.
// TODO: This should be generated by the controller and not user-provided.
WebhookURL string `json:"webhookURL,omitempty"`
// WebhookSecret is the secret used to validate incoming webhooks.
// TODO: this should be generated by the controller and not user-provided.
WebhookSecret string `json:"webhookSecret,omitempty"`
@@ -47,6 +47,13 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
Format: "",
},
},
"branch": {
SchemaProps: spec.SchemaProps{
Description: "The branch to use in the repository. By default, this is the main branch.",
Type: []string{"string"},
Format: "",
},
},
"token": {
SchemaProps: spec.SchemaProps{
Description: "Token for accessing the repository.",
@@ -54,6 +61,13 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
Format: "",
},
},
"webhookURL": {
SchemaProps: spec.SchemaProps{
Description: "WebhookURL is the URL to send webhooks events to. By default, the system will generate a URL for you but you can use this one to run grafana locally and test the webhooks.",
Type: []string{"string"},
Format: "",
},
},
"webhookSecret": {
SchemaProps: spec.SchemaProps{
Description: "WebhookSecret is the secret used to validate incoming webhooks.",
+266 -45
View File
@@ -2,26 +2,41 @@ package provisioning
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/google/go-github/v66/github"
"golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
type githubRepository struct {
config *provisioning.Repository
logger *slog.Logger
config *provisioning.Repository
githubClient *github.Client
}
func newGithubRepository(config *provisioning.Repository) *githubRepository {
return &githubRepository{config}
func newGithubRepository(ctx context.Context, config *provisioning.Repository) *githubRepository {
tokenSrc := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: config.Spec.GitHub.Token},
)
tokenClient := oauth2.NewClient(ctx, tokenSrc)
githubClient := github.NewClient(tokenClient)
return &githubRepository{
config: config,
githubClient: githubClient,
logger: slog.Default().With("logger", "github-repository"),
}
}
func (r *githubRepository) Config() *provisioning.Repository {
@@ -47,12 +62,24 @@ func (r *githubRepository) Validate() (list field.ErrorList) {
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"))
}
if gh.WebhookURL == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "webhookURL"), "a webhook URL is required"))
}
if gh.WebhookSecret == "" {
list = append(list, field.Required(field.NewPath("spec", "github", "webhookSecret"), "a webhook secret is required"))
}
_, err := url.Parse(gh.WebhookURL)
if err != nil {
list = append(list, field.Invalid(field.NewPath("spec", "github", "webhookURL"), gh.WebhookURL, "invalid URL"))
}
return list
}
// Test implements provisioning.Repository.
func (r *githubRepository) Test(ctx context.Context) error {
return &errors.StatusError{
return &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "test is not yet implemented",
Code: http.StatusNotImplemented,
@@ -60,58 +87,252 @@ func (r *githubRepository) Test(ctx context.Context) error {
}
}
// 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) Read(ctx context.Context, filePath string, commit string) ([]byte, 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{
content, _, _, err := r.githubClient.Repositories.GetContents(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, filePath, &github.RepositoryContentGetOptions{
Ref: commit,
})
if err != nil {
// TODO: return bad request status code
return nil, fmt.Errorf("failed to get content: %w", err)
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "file not found",
Code: http.StatusNotFound,
},
}
}
return nil, fmt.Errorf("get contents: %w", err)
}
data, err := content.GetContent()
if err != nil {
return nil, err
return nil, fmt.Errorf("get content: %w", err)
}
return []byte(data), 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))
func (r *githubRepository) Webhook(responder rest.Responder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
payload, err := github.ValidatePayload(req, []byte(r.config.Spec.GitHub.WebhookSecret))
if err != nil {
responder.Error(apierrors.NewUnauthorized("invalid signature"))
return
}
event, err := github.ParseWebHook(github.WebHookType(req), payload)
if err != nil {
responder.Error(apierrors.NewBadRequest("invalid payload"))
return
}
switch event := event.(type) {
case *github.PushEvent:
if err := r.onPushEvent(req.Context(), event); err != nil {
responder.Error(err)
return
}
responder.Object(200, &metav1.Status{
Message: "event processed",
Code: http.StatusOK,
})
default:
responder.Error(apierrors.NewBadRequest("unsupported event type"))
}
}
}
func (r *githubRepository) onPushEvent(ctx context.Context, event *github.PushEvent) error {
if event.GetRepo() == nil {
return fmt.Errorf("missing repository in push event")
}
if event.GetRepo().GetFullName() != fmt.Sprintf("%s/%s", r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository) {
return fmt.Errorf("repository mismatch")
}
branch := r.config.Spec.GitHub.Branch
if branch == "" {
// Default to main if no branch is specified
// TODO: should this be a mutation hook instead?
branch = "main"
}
// Skip silently if the event is not for the main/master branch
// as we cannot configure the webhook to only publish events for the main branch
if event.GetRef() != fmt.Sprintf("refs/heads/%s", branch) {
return nil
}
beforeRef := event.GetBefore()
for _, commit := range event.Commits {
r.logger.Info("process commit", "commit", commit.GetID(), "message", commit.GetMessage())
for _, file := range commit.Added {
resource, err := r.Read(ctx, file, commit.GetID())
if err != nil {
return fmt.Errorf("read added resource: %w", err)
}
r.logger.Info("added file", "file", file, "resource", resource, "commit", commit.GetID())
}
for _, file := range commit.Modified {
resource, err := r.Read(ctx, file, commit.GetID())
if err != nil {
return fmt.Errorf("read modified resource: %w", err)
}
r.logger.Info("modified file", "file", file, "resource", resource, "commit", commit.GetID())
}
for _, file := range commit.Removed {
resource, err := r.Read(ctx, file, beforeRef)
if err != nil {
return fmt.Errorf("read removed resource: %w", err)
}
r.logger.Info("removed file", "file", file, "resource", resource, "commit", commit.GetID())
}
beforeRef = commit.GetID()
}
return nil
}
func (r *githubRepository) createWebhook(ctx context.Context) error {
hook := &github.Hook{
Config: &github.HookConfig{
URL: github.String(r.config.Spec.GitHub.WebhookURL),
ContentType: github.String("json"),
Secret: github.String(r.config.Spec.GitHub.WebhookSecret),
},
Events: []string{"push"},
Active: github.Bool(true),
}
if _, _, err := r.githubClient.Repositories.CreateHook(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, hook); err != nil {
return err
}
r.logger.Info("webhook created", "url", r.config.Spec.GitHub.WebhookURL)
return nil
}
func (r *githubRepository) updateWebhook(ctx context.Context, oldRepo *githubRepository) (UndoFunc, error) {
owner := r.config.Spec.GitHub.Owner
repoName := r.config.Spec.GitHub.Repository
hooks, _, err := r.githubClient.Repositories.ListHooks(ctx, owner, repoName, nil)
if err != nil {
return nil, fmt.Errorf("list existing webhooks: %w", err)
}
newCfg := r.config.Spec.GitHub
oldCfg := oldRepo.Config().Spec.GitHub
switch {
case newCfg.WebhookURL != oldCfg.WebhookURL:
// In this case we cannot find out out which webhook to update, so we delete the old one and create a new one
if err := r.createWebhook(ctx); err != nil {
return nil, fmt.Errorf("create new webhook: %w", err)
}
undoFunc := UndoFunc(func(ctx context.Context) error {
if err := r.deleteWebhook(ctx); err != nil {
return fmt.Errorf("revert create new webhook: %w", err)
}
r.logger.Info("create new webhook reverted", "url", newCfg.WebhookURL)
return nil
})
if err := oldRepo.deleteWebhook(ctx); err != nil {
return undoFunc, fmt.Errorf("delete old webhook: %w", err)
}
undoFunc = undoFunc.Chain(ctx, func(ctx context.Context) error {
if err := oldRepo.createWebhook(ctx); err != nil {
return fmt.Errorf("revert delete old webhook: %w", err)
}
r.logger.Info("delete old webhook reverted", "url", oldCfg.WebhookURL)
return nil
})
return undoFunc, nil
case newCfg.WebhookSecret != oldCfg.WebhookSecret:
for _, hook := range hooks {
if *hook.Config.URL == oldCfg.WebhookURL {
hook.Config.Secret = github.String(newCfg.WebhookSecret)
_, _, err := r.githubClient.Repositories.EditHook(ctx, owner, repoName, *hook.ID, hook)
if err != nil {
return nil, fmt.Errorf("update webhook secret: %w", err)
}
r.logger.Info("webhook secret updated", "url", newCfg.WebhookURL)
return func(ctx context.Context) error {
hook.Config.Secret = github.String(oldCfg.WebhookSecret)
if _, _, err := r.githubClient.Repositories.EditHook(ctx, owner, repoName, *hook.ID, hook); err != nil {
return fmt.Errorf("revert webhook secret: %w", err)
}
r.logger.Info("webhook secret reverted", "url", oldCfg.WebhookURL)
return nil
}, nil
}
}
return nil, errors.New("webhook not found")
default:
return nil, nil
}
}
func (r *githubRepository) deleteWebhook(ctx context.Context) error {
owner := r.config.Spec.GitHub.Owner
name := r.config.Spec.GitHub.Repository
hooks, _, err := r.githubClient.Repositories.ListHooks(ctx, owner, name, nil)
if err != nil {
return fmt.Errorf("list existing webhooks: %w", err)
}
for _, hook := range hooks {
if *hook.Config.URL == r.config.Spec.GitHub.WebhookURL {
if _, err := r.githubClient.Repositories.DeleteHook(ctx, owner, name, *hook.ID); err != nil {
return fmt.Errorf("delete webhook: %w", err)
}
}
}
r.logger.Info("webhook deleted", "url", r.config.Spec.GitHub.WebhookURL)
return nil
}
func (r *githubRepository) AfterCreate(ctx context.Context) error {
return r.createWebhook(ctx)
}
func (r *githubRepository) BeginUpdate(ctx context.Context, old Repository) (UndoFunc, error) {
oldGitRepo, ok := old.(*githubRepository)
if !ok {
return nil, fmt.Errorf("old repository is not a github repository")
}
return r.updateWebhook(ctx, oldGitRepo)
}
func (r *githubRepository) AfterDelete(ctx context.Context) error {
return r.deleteWebhook(ctx)
}
+14 -1
View File
@@ -10,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
@@ -131,7 +132,19 @@ func (r *localRepository) Read(ctx context.Context, path string, commit string)
}
// Webhook implements provisioning.Repository.
func (r *localRepository) Webhook() http.HandlerFunc {
func (r *localRepository) Webhook(responder rest.Responder) http.HandlerFunc {
// webhooks are not supported with local
return nil
}
func (r *localRepository) AfterCreate(ctx context.Context) error {
return nil
}
func (r *localRepository) BeginUpdate(ctx context.Context, old Repository) (UndoFunc, error) {
return nil, nil
}
func (r *localRepository) AfterDelete(ctx context.Context) error {
return nil
}
+103 -6
View File
@@ -3,6 +3,7 @@ package provisioning
import (
"context"
"fmt"
"log/slog"
"path/filepath"
"slices"
@@ -20,6 +21,9 @@ import (
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/registry/generic/registry"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
@@ -36,11 +40,13 @@ var (
type ProvisioningAPIBuilder struct {
getter rest.Getter
localFileResolver *LocalFolderResolver
logger *slog.Logger
}
func NewProvisioningAPIBuilder(local *LocalFolderResolver) *ProvisioningAPIBuilder {
return &ProvisioningAPIBuilder{
localFileResolver: local,
logger: slog.Default().With("logger", "provisioning-api-builder"),
}
}
@@ -99,6 +105,12 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
if err != nil {
return fmt.Errorf("failed to create repository storage: %w", err)
}
repositoryStorage.AfterCreate = b.afterCreate
// AfterUpdate doesn't have the old object, so we have to use BeginUpdate
repositoryStorage.BeginUpdate = b.beginUpdate
repositoryStorage.AfterDelete = b.afterDelete
repositoryStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, repositoryStorage)
b.getter = repositoryStorage
@@ -127,10 +139,10 @@ func (b *ProvisioningAPIBuilder) GetRepository(ctx context.Context, name string)
if err != nil {
return nil, err
}
return b.asRepository(obj)
return b.asRepository(ctx, obj)
}
func (b *ProvisioningAPIBuilder) asRepository(obj runtime.Object) (Repository, error) {
func (b *ProvisioningAPIBuilder) asRepository(ctx context.Context, obj runtime.Object) (Repository, error) {
if obj == nil {
return nil, fmt.Errorf("missing repository object")
}
@@ -143,12 +155,86 @@ func (b *ProvisioningAPIBuilder) asRepository(obj runtime.Object) (Repository, e
case provisioning.LocalRepositoryType:
return newLocalRepository(r, b.localFileResolver), nil
case provisioning.GithubRepositoryType:
return newGithubRepository(r), nil
return newGithubRepository(ctx, r), nil
case provisioning.S3RepositoryType:
return newS3Repository(r), nil
default:
return &unknownRepository{config: r}, nil
}
}
func (b *ProvisioningAPIBuilder) afterCreate(obj runtime.Object, opts *metav1.CreateOptions) {
cfg, ok := obj.(*provisioning.Repository)
if !ok {
b.logger.Error("object is not *provisioning.Repository")
return
}
ctx := context.Background()
repo, err := b.asRepository(ctx, cfg)
if err != nil {
b.logger.Error("failed to get repository", "error", err)
return
}
if err := repo.AfterCreate(ctx); err != nil {
b.logger.Error("failed to run after create", "error", err)
return
}
}
func (b *ProvisioningAPIBuilder) beginUpdate(ctx context.Context, obj, old runtime.Object, opts *metav1.UpdateOptions) (registry.FinishFunc, error) {
objCfg, ok := obj.(*provisioning.Repository)
if !ok {
return nil, fmt.Errorf("new object is not *provisioning.Repository")
}
oldCfg, ok := old.(*provisioning.Repository)
if !ok {
return nil, fmt.Errorf("old object is not *provisioning.Repository")
}
repo, err := b.asRepository(ctx, objCfg)
if err != nil {
return nil, fmt.Errorf("get new repository: %w", err)
}
oldRepo, err := b.asRepository(ctx, oldCfg)
if err != nil {
return nil, fmt.Errorf("get old repository: %w", err)
}
undo, err := repo.BeginUpdate(ctx, oldRepo)
if err != nil {
return nil, err
}
return func(ctx context.Context, success bool) {
if !success && undo != nil {
if err := undo(ctx); err != nil {
b.logger.Error("failed to undo failed update", "error", err)
}
}
}, nil
}
func (b *ProvisioningAPIBuilder) afterDelete(obj runtime.Object, opts *metav1.DeleteOptions) {
ctx := context.Background()
cfg, ok := obj.(*provisioning.Repository)
if !ok {
b.logger.Error("object is not *provisioning.Repository")
return
}
repo, err := b.asRepository(ctx, cfg)
if err != nil {
b.logger.Error("failed to get repository", "error", err)
return
}
if err := repo.AfterDelete(ctx); err != nil {
b.logger.Error("failed to run after delete", "error", err)
return
}
return &unknownRepository{config: r}, nil
}
func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
@@ -157,16 +243,27 @@ func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attri
return nil // This is normal for sub-resource
}
repo, err := b.asRepository(obj)
repo, err := b.asRepository(ctx, obj)
if err != nil {
return err
}
// Typed validation
list := repo.Validate()
cfg := repo.Config()
if a.GetOperation() == admission.Update {
oldRepo, err := b.asRepository(ctx, a.GetOldObject())
if err != nil {
return fmt.Errorf("get old repository for update: %w", err)
}
if cfg.Spec.Type != oldRepo.Config().Spec.Type {
list = append(list, field.Invalid(field.NewPath("spec", "type"),
cfg.Spec.Type, "Changing repository type is not supported"))
}
}
if cfg.Spec.Title == "" {
list = append(list, field.Required(field.NewPath("spec", "title"), "a repository title must be given"))
}
+14 -1
View File
@@ -7,6 +7,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
@@ -60,7 +61,19 @@ func (r *s3Repository) Read(ctx context.Context, path string, commit string) ([]
}
// Webhook implements provisioning.Repository.
func (r *s3Repository) Webhook() http.HandlerFunc {
func (r *s3Repository) Webhook(responder rest.Responder) http.HandlerFunc {
// webhooks are not supported with local
return nil
}
func (r *s3Repository) AfterCreate(ctx context.Context) error {
return nil
}
func (r *s3Repository) BeginUpdate(ctx context.Context, old Repository) (UndoFunc, error) {
return func(ctx context.Context) error { return nil }, nil
}
func (r *s3Repository) AfterDelete(ctx context.Context) error {
return nil
}
+33 -2
View File
@@ -7,6 +7,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
@@ -15,6 +16,19 @@ type RepoGetter interface {
GetRepository(ctx context.Context, name string) (Repository, error)
}
// UndoFunc is a function that can be called to undo a previous operation
type UndoFunc func(context.Context) error
// Chain is a helper function to chain UndoFuncs together
func (f UndoFunc) Chain(ctx context.Context, next UndoFunc) UndoFunc {
return func(ctx context.Context) error {
if err := f(ctx); err != nil {
return err
}
return next(ctx)
}
}
type Repository interface {
// The saved object
Config() *provisioning.Repository
@@ -31,7 +45,12 @@ type Repository interface {
Read(ctx context.Context, path string, commit string) ([]byte, error)
// For repositories that support webhooks
Webhook() http.HandlerFunc
Webhook(responder rest.Responder) http.HandlerFunc
// Hooks called after the repository has been created, updated or deleted
AfterCreate(ctx context.Context) error
BeginUpdate(ctx context.Context, old Repository) (UndoFunc, error)
AfterDelete(ctx context.Context) error
}
var _ Repository = (*unknownRepository)(nil)
@@ -77,7 +96,19 @@ func (r *unknownRepository) Read(ctx context.Context, path string, commit string
}
// Webhook implements provisioning.Repository.
func (r *unknownRepository) Webhook() http.HandlerFunc {
func (r *unknownRepository) Webhook(responder rest.Responder) http.HandlerFunc {
// webhooks are not supported with local
return nil
}
func (r *unknownRepository) AfterCreate(ctx context.Context) error {
return nil
}
func (r *unknownRepository) BeginUpdate(ctx context.Context, old Repository) (UndoFunc, error) {
return nil, nil
}
func (r *unknownRepository) AfterDelete(ctx context.Context) error {
return nil
}
+1 -1
View File
@@ -57,7 +57,7 @@ func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtim
if err != nil {
return nil, err
}
webhook := repo.Webhook()
webhook := repo.Webhook(responder)
if webhook == nil {
return nil, &errors.StatusError{
ErrStatus: v1.Status{
@@ -26,6 +26,7 @@ func (b *builderAdmission) Validate(ctx context.Context, a admission.Attributes,
if v, ok := b.validators[a.GetResource().GroupVersion()]; ok {
return v.Validate(ctx, a, o)
}
return nil
}
@@ -155,11 +155,13 @@ func TestIntegrationProvisioning(t *testing.T) {
"github-example": {
"description": "load resources from github",
"github": {
"branch": "dummy-branch",
"branchWorkflow": true,
"generateDashboardPreviews": true,
"owner": "grafana",
"repository": "git-ui-sync-demo",
"token": "github_pat_dummy",
"webhookURL": "https://dummyWebhookUrl/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/github-example/webhook",
"webhookSecret": "dummyWebhookSecret"
},
"title": "Github Example",
@@ -9,7 +9,9 @@ spec:
github:
owner: grafana
repository: git-ui-sync-demo
branch: dummy-branch
branchWorkflow: true
generateDashboardPreviews: true
token: "github_pat_dummy"
webhookUrl: "https://dummyWebhookUrl/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/github-example/webhook"
webhookSecret: "dummyWebhookSecret"