Provisioning: add webhook support in API Server and Operator (#110673)

* Provisioning: add webhook support in API Server

* updating Extra interface

* adding extra with workers interface

* reverting extraWithWorkers in RegisterAPIService

* adding extra job worker provider

* adding new extra job provider

* Wire things differently

* Remove unused GetJobs

* Pass url variable as string

* Support webhooks in controller

* Fix condition

* Change the naming

---------

Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
This commit is contained in:
Daniele Stefano Ferru 2025-09-08 19:39:05 +02:00 committed by GitHub
parent 32e997d282
commit 76976ef648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 144 additions and 38 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/apps/provisioning/pkg/repository/github"
"github.com/grafana/grafana/apps/provisioning/pkg/repository/local"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
)
@ -55,6 +56,7 @@ type provisioningControllerConfig struct {
// audiences =
// [operator]
// provisioning_server_url =
// provisioning_server_public_url =
// dashboards_server_url =
// folders_server_url =
// tls_insecure =
@ -233,12 +235,16 @@ func setupRepoFactory(
switch provisioning.RepositoryType(t) {
case provisioning.GitHubRepositoryType:
var webhook *webhooks.WebhookExtraBuilder
provisioningAppURL := operatorSec.Key("provisioning_server_public_url").String()
if provisioningAppURL != "" {
webhook = webhooks.ProvideWebhooks(provisioningAppURL)
}
extras = append(extras, github.Extra(
decrypter,
github.ProvideFactory(),
// TODO: we need to plug the webhook builder here for webhooks to be created in repository controller
// https://github.com/grafana/git-ui-sync-project/issues/455
nil,
webhook,
),
)
case provisioning.LocalRepositoryType:

View File

@ -3,7 +3,6 @@ package provisioning
import (
"context"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kube-openapi/pkg/spec3"
@ -13,7 +12,6 @@ type Extra interface {
Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error)
UpdateStorage(storage map[string]rest.Storage) error
PostProcessOpenAPI(oas *spec3.OpenAPI) error
GetJobWorkers() []jobs.Worker
}
type ExtraBuilder func(b *APIBuilder) Extra

View File

@ -6,12 +6,14 @@ import (
"github.com/grafana/grafana/apps/provisioning/pkg/repository/local"
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
"github.com/grafana/grafana/pkg/setting"
)
// HACK: This is a hack so that wire can uniquely identify dependencies
func ProvideProvisioningOSSExtras(webhook *webhooks.WebhookExtraBuilder) []provisioning.ExtraBuilder {
func ProvideProvisioningExtraAPIs(webhook *webhooks.WebhookExtraBuilder) []provisioning.ExtraBuilder {
return []provisioning.ExtraBuilder{
webhook.ExtraBuilder,
}
@ -35,3 +37,7 @@ func ProvideProvisioningOSSRepositoryExtras(
),
}
}
func ProvideExtraWorkers(pullRequestWorker *pullrequest.PullRequestWorker) []jobs.Worker {
return []jobs.Worker{pullRequestWorker}
}

View File

@ -108,7 +108,8 @@ type APIBuilder struct {
statusPatcher *appcontroller.RepositoryStatusPatcher
healthChecker *controller.HealthChecker
// Extras provides additional functionality to the API.
extras []Extra
extras []Extra
extraWorkers []jobs.Worker
}
// NewAPIBuilder creates an API builder.
@ -126,6 +127,7 @@ func NewAPIBuilder(
access authlib.AccessChecker,
tracer tracing.Tracer,
extraBuilders []ExtraBuilder,
extraWorkers []jobs.Worker,
jobHistoryConfig *JobHistoryConfig,
) *APIBuilder {
clients := resources.NewClientFactory(configProvider)
@ -147,6 +149,7 @@ func NewAPIBuilder(
unified: unified,
access: access,
jobHistoryConfig: jobHistoryConfig,
extraWorkers: extraWorkers,
}
for _, builder := range extraBuilders {
@ -204,6 +207,7 @@ func RegisterAPIService(
usageStats usagestats.Service,
tracer tracing.Tracer,
extraBuilders []ExtraBuilder,
extraWorkers []jobs.Worker,
repoFactory repository.Factory,
) (*APIBuilder, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
@ -221,6 +225,7 @@ func RegisterAPIService(
access,
tracer,
extraBuilders,
extraWorkers,
createJobHistoryConfigFromSettings(cfg),
)
apiregistration.RegisterAPI(builder)
@ -720,9 +725,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
}
// Add any extra workers
for _, extra := range b.extras {
workers = append(workers, extra.GetJobWorkers()...)
}
workers = append(workers, b.extraWorkers...)
var jobHistoryWriter jobs.HistoryWriter
if b.jobHistoryLoki != nil {

View File

@ -106,3 +106,19 @@ func (r *screenshotRenderer) RenderScreenshot(ctx context.Context, repo provisio
return fmt.Sprintf("apis/%s/namespaces/%s/repositories/%s/render/%s",
provisioning.APIVERSION, repo.Namespace, repo.Name, rsp.Uid), nil
}
type NoOpRenderer struct{}
func NewNoOpRenderer() ScreenshotRenderer {
return &NoOpRenderer{}
}
func (r *NoOpRenderer) IsAvailable(_ context.Context) bool {
return false
}
func (r *NoOpRenderer) RenderScreenshot(
_ context.Context, _ provisioning.ResourceRepositoryInfo, _ string, _ url.Values,
) (string, error) {
return "", nil
}

View File

@ -12,8 +12,33 @@ import (
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func ProvidePullRequestWorker(
cfg *setting.Cfg,
renderer rendering.Service,
blobstore resource.ResourceClient,
configProvider apiserver.RestConfigProvider,
) *PullRequestWorker {
urlProvider := func(_ string) string {
return cfg.AppURL
}
// FIXME: we should create providers for client and parsers, so that we don't have
// multiple connections for webhooks
clients := resources.NewClientFactory(configProvider)
parsers := resources.NewParserFactory(clients)
screenshotRenderer := NewScreenshotRenderer(renderer, blobstore)
evaluator := NewEvaluator(screenshotRenderer, parsers, urlProvider)
commenter := NewCommenter()
return NewPullRequestWorker(evaluator, commenter)
}
//go:generate mockery --name=PullRequestRepo --structname=MockPullRequestRepo --inpackage --filename=mock_pullrequest_repo.go --with-expecter
type PullRequestRepo interface {
Config() *provisioning.Repository

View File

@ -28,6 +28,7 @@ type WebhookExtraBuilder struct {
urlProvider func(namespace string) string
}
// FIXME: separate the URL provider from connector to simplify operators
func (b *WebhookExtraBuilder) WebhookURL(ctx context.Context, r *provisioning.Repository) string {
if !b.isPublic {
return ""
@ -57,7 +58,7 @@ func isPublicURL(url string) bool {
!strings.HasPrefix(url, "https://172.16.")
}
func ProvideWebhooks(
func ProvideWebhooksWithImages(
cfg *setting.Cfg,
renderer rendering.Service,
blobstore resource.ResourceClient,
@ -87,7 +88,7 @@ func ProvideWebhooks(
commenter := pullrequest.NewCommenter()
pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter)
return NewWebhookExtra(
return NewWebhookExtraWithImages(
render,
webhook,
urlProvider,
@ -97,21 +98,40 @@ func ProvideWebhooks(
}
}
// WebhookExtra implements the Extra interface for webhooks
func ProvideWebhooks(provisioningURL string) *WebhookExtraBuilder {
urlProvider := func(_ string) string {
return provisioningURL
}
isPublic := isPublicURL(urlProvider(""))
return &WebhookExtraBuilder{
isPublic: isPublic,
urlProvider: urlProvider,
ExtraBuilder: func(b *provisioningapis.APIBuilder) provisioningapis.Extra {
screenshotRenderer := pullrequest.NewNoOpRenderer()
webhook := NewWebhookConnector(isPublic, b, screenshotRenderer)
return NewWebhookExtra(webhook)
},
}
}
// WebhookExtraWithImages implements the Extra interface for webhooks
// to wrap around
type WebhookExtra struct {
type WebhookExtraWithImages struct {
render *renderConnector
webhook *webhookConnector
workers []jobs.Worker
}
func NewWebhookExtra(
func NewWebhookExtraWithImages(
render *renderConnector,
webhook *webhookConnector,
urlProvider func(namespace string) string,
workers []jobs.Worker,
) *WebhookExtra {
return &WebhookExtra{
) *WebhookExtraWithImages {
return &WebhookExtraWithImages{
render: render,
webhook: webhook,
workers: workers,
@ -119,7 +139,7 @@ func NewWebhookExtra(
}
// Authorize delegates authorization to the webhook connector
func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
func (e *WebhookExtraWithImages) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
webhookDecision, webhookReason, webhookErr := e.webhook.Authorize(ctx, a)
if webhookDecision != authorizer.DecisionNoOpinion {
return webhookDecision, webhookReason, webhookErr
@ -129,7 +149,7 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) (
}
// UpdateStorage updates the storage with both render and webhook connectors
func (e *WebhookExtra) UpdateStorage(storage map[string]rest.Storage) error {
func (e *WebhookExtraWithImages) UpdateStorage(storage map[string]rest.Storage) error {
if err := e.webhook.UpdateStorage(storage); err != nil {
return err
}
@ -138,7 +158,7 @@ func (e *WebhookExtra) UpdateStorage(storage map[string]rest.Storage) error {
}
// PostProcessOpenAPI processes OpenAPI specs for both connectors
func (e *WebhookExtra) PostProcessOpenAPI(oas *spec3.OpenAPI) error {
func (e *WebhookExtraWithImages) PostProcessOpenAPI(oas *spec3.OpenAPI) error {
if err := e.webhook.PostProcessOpenAPI(oas); err != nil {
return err
}
@ -146,7 +166,25 @@ func (e *WebhookExtra) PostProcessOpenAPI(oas *spec3.OpenAPI) error {
return e.render.PostProcessOpenAPI(oas)
}
// GetJobWorkers returns job workers from the webhook connector
func (e *WebhookExtra) GetJobWorkers() []jobs.Worker {
return e.workers
type WebhookExtra struct {
webhook *webhookConnector
}
func NewWebhookExtra(webhook *webhookConnector) *WebhookExtra {
return &WebhookExtra{webhook: webhook}
}
// Authorize delegates authorization to the webhook connector
func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return e.webhook.Authorize(ctx, a)
}
// UpdateStorage updates the storage with webhook connector
func (e *WebhookExtra) UpdateStorage(storage map[string]rest.Storage) error {
return e.webhook.UpdateStorage(storage)
}
// PostProcessOpenAPI processes OpenAPI specs for webhook connectors
func (e *WebhookExtra) PostProcessOpenAPI(oas *spec3.OpenAPI) error {
return e.webhook.PostProcessOpenAPI(oas)
}

View File

@ -3,6 +3,7 @@ package apiregistry
import (
"github.com/google/wire"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
@ -13,6 +14,9 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/preferences"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/registry/apis/service"
@ -27,6 +31,14 @@ var WireSetExts = wire.NewSet(
wire.Bind(new(iam.RoleStorageBackend), new(*noopstorage.StorageBackendImpl)),
)
var provisioningExtras = wire.NewSet(
repository.ProvideFactory,
pullrequest.ProvidePullRequestWorker,
webhooks.ProvideWebhooksWithImages,
extras.ProvideProvisioningExtraAPIs,
extras.ProvideExtraWorkers,
)
var WireSet = wire.NewSet(
ProvideRegistryServiceSink, // dummy background service that forces registration
@ -37,6 +49,8 @@ var WireSet = wire.NewSet(
// Secrets
secret.RegisterDependencies,
// Provisioning
provisioningExtras,
// Each must be added here *and* in the ServiceSink above
dashboardinternal.RegisterAPIService,

View File

@ -63,6 +63,7 @@ import (
provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
query2 "github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/registry/apis/secret/clock"
@ -814,8 +815,10 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder)
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v4 := extras.ProvideExtraWorkers(pullRequestWorker)
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer)
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
if err != nil {
@ -826,12 +829,12 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
return nil, err
}
factory := github.ProvideFactory()
v4 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder)
repositoryFactory, err := repository.ProvideFactory(v4)
v5 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder)
repositoryFactory, err := repository.ProvideFactory(v5)
if err != nil {
return nil, err
}
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory)
if err != nil {
return nil, err
}
@ -1401,8 +1404,10 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder)
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v4 := extras.ProvideExtraWorkers(pullRequestWorker)
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer)
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
if err != nil {
@ -1413,12 +1418,12 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
return nil, err
}
factory := github.ProvideFactory()
v4 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder)
repositoryFactory, err := repository.ProvideFactory(v4)
v5 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder)
repositoryFactory, err := repository.ProvideFactory(v5)
if err != nil {
return nil, err
}
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory)
if err != nil {
return nil, err
}

View File

@ -7,7 +7,6 @@ package server
import (
"github.com/google/wire"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/pkg/configprovider"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing"
@ -16,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/registry"
apisregistry "github.com/grafana/grafana/pkg/registry/apis"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
gsmKMSProviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
@ -69,9 +67,6 @@ import (
)
var provisioningExtras = wire.NewSet(
webhooks.ProvideWebhooks,
repository.ProvideFactory,
extras.ProvideProvisioningOSSExtras,
extras.ProvideProvisioningOSSRepositoryExtras,
)