mirror of https://github.com/rancher/gitjob.git
380 lines
11 KiB
Go
380 lines
11 KiB
Go
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
goPlaygroundAzuredevops "github.com/go-playground/webhooks/v6/azuredevops"
|
|
"github.com/rancher/gitjob/pkg/webhook/azuredevops"
|
|
|
|
"github.com/go-logr/logr"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
|
|
"github.com/gorilla/mux"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kcache "k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/util/retry"
|
|
"sigs.k8s.io/controller-runtime/pkg/cache"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
gogsclient "github.com/gogits/go-gogs-client"
|
|
v1 "github.com/rancher/gitjob/pkg/apis/gitjob.cattle.io/v1"
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/go-playground/webhooks.v5/bitbucket"
|
|
bitbucketserver "gopkg.in/go-playground/webhooks.v5/bitbucket-server"
|
|
"gopkg.in/go-playground/webhooks.v5/github"
|
|
"gopkg.in/go-playground/webhooks.v5/gitlab"
|
|
"gopkg.in/go-playground/webhooks.v5/gogs"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
ktypes "k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
const (
|
|
webhookSecretName = "gitjob-webhook" //nolint:gosec // this is a resource name
|
|
webhookDefaultSyncInterval = 3600
|
|
githubKey = "github"
|
|
gitlabKey = "gitlab"
|
|
bitbucketKey = "bitbucket"
|
|
bitbucketServerKey = "bitbucket-server"
|
|
gogsKey = "gogs"
|
|
azureUsername = "azure-username"
|
|
azurePassword = "azure-password"
|
|
|
|
branchRefPrefix = "refs/heads/"
|
|
tagRefPrefix = "refs/tags/"
|
|
)
|
|
|
|
type Webhook struct {
|
|
client client.Client
|
|
namespace string
|
|
github *github.Webhook
|
|
gitlab *gitlab.Webhook
|
|
bitbucket *bitbucket.Webhook
|
|
bitbucketServer *bitbucketserver.Webhook
|
|
gogs *gogs.Webhook
|
|
log logr.Logger
|
|
azureDevops *azuredevops.Webhook
|
|
}
|
|
|
|
func New(namespace string, client client.Client) (*Webhook, error) {
|
|
webhook := &Webhook{
|
|
client: client,
|
|
namespace: namespace,
|
|
log: ctrl.Log.WithName("webhook"),
|
|
}
|
|
err := webhook.initGitProviders()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return webhook, nil
|
|
}
|
|
|
|
func (w *Webhook) initGitProviders() error {
|
|
var err error
|
|
|
|
w.github, err = github.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.gitlab, err = gitlab.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.bitbucket, err = bitbucket.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.bitbucketServer, err = bitbucketserver.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.gogs, err = gogs.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.azureDevops, err = azuredevops.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *Webhook) onSecretChange(obj interface{}) error {
|
|
secret, ok := obj.(*corev1.Secret)
|
|
if !ok {
|
|
return fmt.Errorf("expected secret object but got %T", obj)
|
|
}
|
|
if secret.Name != webhookSecretName && secret.Namespace != w.namespace {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
github, err := github.New(github.Options.Secret(string(secret.Data[githubKey])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.github = github
|
|
gitlab, err := gitlab.New(gitlab.Options.Secret(string(secret.Data[gitlabKey])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.gitlab = gitlab
|
|
bitbucket, err := bitbucket.New(bitbucket.Options.UUID(string(secret.Data[bitbucketKey])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.bitbucket = bitbucket
|
|
bitbucketServer, err := bitbucketserver.New(bitbucketserver.Options.Secret(string(secret.Data[bitbucketServerKey])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.bitbucketServer = bitbucketServer
|
|
gogs, err := gogs.New(gogs.Options.Secret(string(secret.Data[gogsKey])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.gogs = gogs
|
|
azureDevops, err := azuredevops.New(azuredevops.Options.BasicAuth(string(secret.Data[azureUsername]), string(secret.Data[azurePassword])))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.azureDevops = azureDevops
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
// credit from https://github.com/argoproj/argo-cd/blob/97003caebcaafe1683e71934eb483a88026a4c33/util/webhook/webhook.go#L327-L350
|
|
var payload interface{}
|
|
var err error
|
|
ctx := r.Context()
|
|
|
|
switch {
|
|
//Gogs needs to be checked before Github since it carries both Gogs and (incompatible) Github headers
|
|
case r.Header.Get("X-Gogs-Event") != "":
|
|
payload, err = w.gogs.Parse(r, gogs.PushEvent)
|
|
case r.Header.Get("X-GitHub-Event") != "":
|
|
payload, err = w.github.Parse(r, github.PushEvent)
|
|
case r.Header.Get("X-Gitlab-Event") != "":
|
|
payload, err = w.gitlab.Parse(r, gitlab.PushEvents, gitlab.TagEvents)
|
|
case r.Header.Get("X-Hook-UUID") != "":
|
|
payload, err = w.bitbucket.Parse(r, bitbucket.RepoPushEvent)
|
|
case r.Header.Get("X-Event-Key") != "":
|
|
payload, err = w.bitbucketServer.Parse(r, bitbucketserver.RepositoryReferenceChangedEvent)
|
|
case r.Header.Get("X-Vss-Activityid") != "" || r.Header.Get("X-Vss-Subscriptionid") != "":
|
|
payload, err = w.azureDevops.Parse(r, goPlaygroundAzuredevops.GitPushEventType)
|
|
default:
|
|
logrus.Debug("Ignoring unknown webhook event")
|
|
return
|
|
}
|
|
|
|
logrus.Debugf("Webhook payload %+v", payload)
|
|
|
|
if err != nil {
|
|
logAndReturn(rw, err)
|
|
return
|
|
}
|
|
|
|
var revision, branch, tag string
|
|
var repoURLs []string
|
|
// credit from https://github.com/argoproj/argo-cd/blob/97003caebcaafe1683e71934eb483a88026a4c33/util/webhook/webhook.go#L84-L87
|
|
switch t := payload.(type) {
|
|
case github.PushPayload:
|
|
branch, tag = getBranchTagFromRef(t.Ref)
|
|
revision = t.After
|
|
repoURLs = append(repoURLs, t.Repository.HTMLURL)
|
|
case gitlab.PushEventPayload:
|
|
branch, tag = getBranchTagFromRef(t.Ref)
|
|
revision = t.CheckoutSHA
|
|
repoURLs = append(repoURLs, t.Project.WebURL)
|
|
case gitlab.TagEventPayload:
|
|
branch, tag = getBranchTagFromRef(t.Ref)
|
|
revision = t.CheckoutSHA
|
|
repoURLs = append(repoURLs, t.Project.WebURL)
|
|
// https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/#Push
|
|
case bitbucket.RepoPushPayload:
|
|
repoURLs = append(repoURLs, t.Repository.Links.HTML.Href)
|
|
for _, change := range t.Push.Changes {
|
|
revision = change.New.Target.Hash
|
|
if change.New.Type == "branch" {
|
|
branch = change.New.Name
|
|
} else if change.New.Type == "tag" {
|
|
tag = change.New.Name
|
|
}
|
|
break
|
|
}
|
|
case bitbucketserver.RepositoryReferenceChangedPayload:
|
|
for _, l := range t.Repository.Links["clone"].([]interface{}) {
|
|
link := l.(map[string]interface{})
|
|
if link["name"] == "http" {
|
|
repoURLs = append(repoURLs, link["href"].(string))
|
|
}
|
|
if link["name"] == "ssh" {
|
|
repoURLs = append(repoURLs, link["href"].(string))
|
|
}
|
|
}
|
|
for _, change := range t.Changes {
|
|
revision = change.ToHash
|
|
branch, tag = getBranchTagFromRef(change.ReferenceId)
|
|
break
|
|
}
|
|
case gogsclient.PushPayload:
|
|
repoURLs = append(repoURLs, t.Repo.HTMLURL)
|
|
branch, tag = getBranchTagFromRef(t.Ref)
|
|
revision = t.After
|
|
case goPlaygroundAzuredevops.GitPushEvent:
|
|
repoURLs = append(repoURLs, t.Resource.Repository.RemoteURL)
|
|
for _, refUpdate := range t.Resource.RefUpdates {
|
|
branch, tag = getBranchTagFromRef(refUpdate.Name)
|
|
revision = refUpdate.NewObjectID
|
|
break
|
|
}
|
|
}
|
|
|
|
var gitJobList v1.GitJobList
|
|
err = w.client.List(ctx, &gitJobList, &client.ListOptions{LabelSelector: labels.Everything()})
|
|
if err != nil {
|
|
logAndReturn(rw, err)
|
|
return
|
|
}
|
|
|
|
for _, repo := range repoURLs {
|
|
u, err := url.Parse(repo)
|
|
if err != nil {
|
|
logAndReturn(rw, err)
|
|
return
|
|
}
|
|
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + u.Hostname() + "(:[0-9]+|)[:/]" + u.Path[1:] + "(\\.git)?"
|
|
repoRegexp, err := regexp.Compile(regexpStr)
|
|
if err != nil {
|
|
logAndReturn(rw, err)
|
|
return
|
|
}
|
|
for _, gitjob := range gitJobList.Items {
|
|
if gitjob.Spec.Git.Revision != "" {
|
|
continue
|
|
}
|
|
|
|
if !repoRegexp.MatchString(gitjob.Spec.Git.Repo) {
|
|
continue
|
|
}
|
|
|
|
// if onTag is enabled, we only watch tag event, as it can be coming from any branch
|
|
if gitjob.Spec.Git.OnTag != "" {
|
|
// skipping if gitjob is watching tag only and tag is empty(not a tag event)
|
|
if tag == "" {
|
|
continue
|
|
}
|
|
contraints, err := semver.NewConstraint(gitjob.Spec.Git.OnTag)
|
|
if err != nil {
|
|
logrus.Warnf("Failed to parsing onTag semver from %s/%s, err: %v, skipping", gitjob.Namespace, gitjob.Name, err)
|
|
continue
|
|
}
|
|
v, err := semver.NewVersion(tag)
|
|
if err != nil {
|
|
logrus.Warnf("Failed to parsing semver on incoming tag, err: %v, skipping", err)
|
|
continue
|
|
}
|
|
if !contraints.Check(v) {
|
|
continue
|
|
}
|
|
} else if gitjob.Spec.Git.Branch != "" {
|
|
// else we check if the branch from webhook matches gitjob's branch
|
|
if branch == "" || branch != gitjob.Spec.Git.Branch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if gitjob.Status.Commit != revision && revision != "" {
|
|
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
|
var gitJobFomCluster v1.GitJob
|
|
err := w.client.Get(ctx, ktypes.NamespacedName{Name: gitjob.Name, Namespace: gitjob.Namespace}, &gitJobFomCluster)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gitJobFomCluster.Status.Commit = revision
|
|
// if syncInterval is not set and webhook is configured, set it to 1 hour
|
|
if gitjob.Spec.SyncInterval == 0 {
|
|
gitJobFomCluster.Spec.SyncInterval = webhookDefaultSyncInterval
|
|
}
|
|
return w.client.Status().Update(ctx, &gitJobFomCluster)
|
|
}); err != nil {
|
|
logAndReturn(rw, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rw.WriteHeader(200)
|
|
rw.Write([]byte("succeeded"))
|
|
}
|
|
|
|
func HandleHooks(ctx context.Context, namespace string, client client.Client, clientCache cache.Cache) (http.Handler, error) {
|
|
root := mux.NewRouter()
|
|
webhook, err := New(namespace, client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root.UseEncodedPath()
|
|
root.Handle("/", webhook)
|
|
|
|
var secret corev1.Secret
|
|
informer, err := clientCache.GetInformer(ctx, &secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = informer.AddEventHandler(kcache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) {
|
|
err := webhook.onSecretChange(obj)
|
|
if err != nil {
|
|
webhook.log.Error(err, "new secret added")
|
|
}
|
|
},
|
|
DeleteFunc: func(obj interface{}) {
|
|
err := webhook.initGitProviders()
|
|
if err != nil {
|
|
webhook.log.Error(err, "secret deleted")
|
|
}
|
|
},
|
|
UpdateFunc: func(_, newObj interface{}) {
|
|
err := webhook.onSecretChange(newObj)
|
|
if err != nil {
|
|
webhook.log.Error(err, "secret updated")
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return root, nil
|
|
}
|
|
|
|
func logAndReturn(rw http.ResponseWriter, err error) {
|
|
logrus.Errorf("Webhook processing failed: %s", err)
|
|
rw.WriteHeader(500)
|
|
rw.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
// git ref docs: https://git-scm.com/book/en/v2/Git-Internals-Git-References
|
|
func getBranchTagFromRef(ref string) (string, string) {
|
|
if strings.HasPrefix(ref, branchRefPrefix) {
|
|
return strings.TrimPrefix(ref, branchRefPrefix), ""
|
|
}
|
|
|
|
if strings.HasPrefix(ref, tagRefPrefix) {
|
|
return "", strings.TrimPrefix(ref, tagRefPrefix)
|
|
}
|
|
|
|
return "", ""
|
|
}
|