From a9931ff28343faff26b4331d78ad46db0df5383b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Wed, 20 Nov 2024 10:45:24 +0100 Subject: [PATCH] [Provisioning] Add mutation hook to generate webhook URL and secret (#96742) Add mutation hook to generate webhook URL and secret --- pkg/registry/apis/provisioning/github.go | 12 ++--- pkg/registry/apis/provisioning/register.go | 54 +++++++++++++++++++++- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/pkg/registry/apis/provisioning/github.go b/pkg/registry/apis/provisioning/github.go index 16f0d0c778f..afd382d467f 100644 --- a/pkg/registry/apis/provisioning/github.go +++ b/pkg/registry/apis/provisioning/github.go @@ -56,6 +56,9 @@ func (r *githubRepository) Validate() (list field.ErrorList) { if gh.Repository == "" { list = append(list, field.Required(field.NewPath("spec", "github", "repository"), "a github repo name is required")) } + if gh.Branch == "" { + list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required")) + } if gh.Token == "" { list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required")) } @@ -181,16 +184,9 @@ func (r *githubRepository) onPushEvent(ctx context.Context, event *github.PushEv 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) { + if event.GetRef() != fmt.Sprintf("refs/heads/%s", r.config.Spec.GitHub.Branch) { return nil } diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index c5ada2b3617..39cd04303b7 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -2,6 +2,9 @@ package provisioning import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "fmt" "log/slog" "path/filepath" @@ -39,13 +42,15 @@ var ( // This is used just so wire has something unique to return type ProvisioningAPIBuilder struct { + cfg *setting.Cfg getter rest.Getter localFileResolver *LocalFolderResolver logger *slog.Logger } -func NewProvisioningAPIBuilder(local *LocalFolderResolver) *ProvisioningAPIBuilder { +func NewProvisioningAPIBuilder(cfg *setting.Cfg, local *LocalFolderResolver) *ProvisioningAPIBuilder { return &ProvisioningAPIBuilder{ + cfg: cfg, localFileResolver: local, logger: slog.Default().With("logger", "provisioning-api-builder"), } @@ -60,7 +65,7 @@ func RegisterAPIService( if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis } - builder := NewProvisioningAPIBuilder(&LocalFolderResolver{ + builder := NewProvisioningAPIBuilder(cfg, &LocalFolderResolver{ ProvisioningPath: cfg.ProvisioningPath, DevenvPath: filepath.Join(cfg.HomePath, "devenv"), }) @@ -237,6 +242,38 @@ func (b *ProvisioningAPIBuilder) afterDelete(obj runtime.Object, opts *metav1.De return } } +func (b *ProvisioningAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject() + + if obj == nil || a.GetOperation() == admission.Connect { + return nil // This is normal for sub-resource + } + + r, ok := obj.(*provisioning.Repository) + if !ok { + return fmt.Errorf("expected repository configuration") + } + + if r.Spec.Type == provisioning.GithubRepositoryType { + if r.Spec.GitHub == nil { + return fmt.Errorf("github configuration is required") + } + + if r.Spec.GitHub.Branch == "" { + r.Spec.GitHub.Branch = "main" + } + + if r.Spec.GitHub.WebhookURL == "" { + r.Spec.GitHub.WebhookURL = fmt.Sprintf("%sapi/provisioning/v0alpha1/namespaces/%s/repositories/%s/webhook", b.cfg.AppURL, a.GetNamespace(), a.GetName()) + } + + if r.Spec.GitHub.WebhookSecret == "" { + r.Spec.GitHub.WebhookSecret = b.generateWebhookSecret(r.Spec.GitHub.Token) + } + } + + return nil +} func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { obj := a.GetObject() @@ -449,3 +486,16 @@ spec: return oas, nil } + +// generateWebhookSecret generates a webhook secret from a token +// using the configured secret key. The generated secret is consistent. +// TODO: this must be replaced by a app platform secrets once we have that. +func (b *ProvisioningAPIBuilder) generateWebhookSecret(token string) string { + secretKey := []byte(b.cfg.SecretKey) + h := hmac.New(sha256.New, secretKey) + + h.Write([]byte(token)) + hashed := h.Sum(nil) + + return hex.EncodeToString(hashed) +}