Skip to content

Commit

Permalink
Add azure webhook support
Browse files Browse the repository at this point in the history
  • Loading branch information
raulcabello committed Jan 15, 2024
1 parent b8bad36 commit cb9a0ea
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/webhooks/v6 v6.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/webhooks/v6 v6.3.0 h1:zBLUxK1Scxwi97TmZt5j/B/rLlard2zY7P77FHg58FE=
github.com/go-playground/webhooks/v6 v6.3.0/go.mod h1:GCocmfMtpJdkEOM1uG9p2nXzg1kY5X/LtvQgtPHUaaA=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
Expand All @@ -328,6 +330,7 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogits/go-gogs-client v0.0.0-20200905025246-8bb8a50cb355/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0=
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:04sojTxgYxu1L4Hn7Tgf7UVtIosVa6CuHtvNY+7T1K4=
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0=
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
Expand Down
110 changes: 110 additions & 0 deletions pkg/webhook/azuredevops/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// File copied from https://github.com/go-playground/webhooks/blob/master/azuredevops/azuredevops.go
// TODO Basic Auth is added here since it's not available upstream. Remove ths file once https://github.com/go-playground/webhooks/pull/191 is merged

package azuredevops

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/go-playground/webhooks/v6/azuredevops"
)

// parse errors
var (
ErrInvalidHTTPMethod = errors.New("invalid HTTP Method")
ErrParsingPayload = errors.New("error parsing payload")
ErrBasicAuthVerificationFailed = errors.New("basic auth verification failed")
)

// Option is a configuration option for the webhook
type Option func(*Webhook) error

// Options is a namespace var for configuration options
var Options = WebhookOptions{}

// WebhookOptions is a namespace for configuration option methods
type WebhookOptions struct{}

// BasicAuth verifies payload using basic auth
func (WebhookOptions) BasicAuth(username, password string) Option {
return func(hook *Webhook) error {
hook.username = username
hook.password = password
return nil
}
}

// Webhook instance contains all methods needed to process events
type Webhook struct {
username string
password string
}

// New creates and returns a WebHook instance
func New(options ...Option) (*Webhook, error) {
hook := new(Webhook)
for _, opt := range options {
if err := opt(hook); err != nil {
return nil, errors.New("Error applying Option")
}
}
return hook, nil
}

// Parse verifies and parses the events specified and returns the payload object or an error
func (hook Webhook) Parse(r *http.Request, events ...azuredevops.Event) (interface{}, error) {
defer func() {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}()

if !hook.verifyBasicAuth(r) {
return nil, ErrBasicAuthVerificationFailed
}

if r.Method != http.MethodPost {
return nil, ErrInvalidHTTPMethod
}

payload, err := io.ReadAll(r.Body)
if err != nil || len(payload) == 0 {
return nil, ErrParsingPayload
}

var pl azuredevops.BasicEvent
err = json.Unmarshal([]byte(payload), &pl)
if err != nil {
return nil, ErrParsingPayload
}

switch pl.EventType {
case azuredevops.GitPushEventType:
var fpl azuredevops.GitPushEvent
err = json.Unmarshal([]byte(payload), &fpl)
return fpl, err
case azuredevops.GitPullRequestCreatedEventType, azuredevops.GitPullRequestMergedEventType, azuredevops.GitPullRequestUpdatedEventType:
var fpl azuredevops.GitPullRequestEvent
err = json.Unmarshal([]byte(payload), &fpl)
return fpl, err
case azuredevops.BuildCompleteEventType:
var fpl azuredevops.BuildCompleteEvent
err = json.Unmarshal([]byte(payload), &fpl)
return fpl, err
default:
return nil, fmt.Errorf("unknown event %s", pl.EventType)
}
}

func (hook Webhook) verifyBasicAuth(r *http.Request) bool {
// skip validation if username or password was not provided
if hook.username == "" && hook.password == "" {
return true
}
username, password, ok := r.BasicAuth()

return ok && username == hook.username && password == hook.password
}
20 changes: 20 additions & 0 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"regexp"
"strings"

goPlaygroundAzuredevops "github.com/go-playground/webhooks/v6/azuredevops"
"github.com/rancher/gitjob/pkg/webhook/azuredevops"

"github.com/Masterminds/semver/v3"
gogsclient "github.com/gogits/go-gogs-client"
"github.com/gorilla/mux"
Expand All @@ -31,6 +34,8 @@ const (
bitbucketKey = "bitbucket"
bitbucketServerKey = "bitbucket-server"
gogsKey = "gogs"
azureUsername = "azure-username"
azurePassword = "azure-password"

branchRefPrefix = "refs/heads/"
tagRefPrefix = "refs/tags/"
Expand All @@ -46,6 +51,7 @@ type Webhook struct {
bitbucket *bitbucket.Webhook
bitbucketServer *bitbucketserver.Webhook
gogs *gogs.Webhook
azureDevops *azuredevops.Webhook
}

func New(ctx context.Context, rContext *types.Context) *Webhook {
Expand Down Expand Up @@ -89,6 +95,11 @@ func (w *Webhook) onSecretChange(_ string, secret *corev1.Secret) (*corev1.Secre
if err != nil {
return nil, err
}
w.azureDevops, err = azuredevops.New(azuredevops.Options.BasicAuth(string(secret.Data[azureUsername]), string(secret.Data[azurePassword])))
if err != nil {
return nil, err
}

return nil, nil
}

Expand All @@ -109,6 +120,8 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
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
Expand Down Expand Up @@ -168,6 +181,13 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
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
}
}

gitjobs, err := w.gitjobs.Cache().List("", labels.Everything())
Expand Down
61 changes: 61 additions & 0 deletions pkg/webhook/webhook_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package webhook

import (
"bytes"

"github.com/golang/mock/gomock"
v1 "github.com/rancher/gitjob/pkg/apis/gitjob.cattle.io/v1"
"github.com/rancher/gitjob/pkg/webhook/azuredevops"
"github.com/rancher/wrangler/v2/pkg/generic/fake"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"

"net/http"
"testing"

"gotest.tools/assert"
Expand Down Expand Up @@ -31,3 +41,54 @@ func TestGetBranchTagFromRef(t *testing.T) {
assert.Equal(t, tag, outputs[i][1])
}
}

func TestAzureDevopsWebhook(t *testing.T) {
const commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
const repoURL = "https://dev.azure.com/fleet/git-test/_git/git-test"
ctrl := gomock.NewController(t)
mockGitjob := fake.NewMockControllerInterface[*v1.GitJob, *v1.GitJobList](ctrl)
mockGitjobCache := fake.NewMockCacheInterface[*v1.GitJob](ctrl)
gitjob := &v1.GitJob{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: v1.GitJobSpec{
Git: v1.GitInfo{
Repo: repoURL,
Branch: "main",
},
},
}
gitjobs := []*v1.GitJob{gitjob}
w := &Webhook{gitjobs: mockGitjob}
w.azureDevops, _ = azuredevops.New()
jsonBody := []byte(`{"subscriptionId":"xxx","notificationId":1,"id":"xxx","eventType":"git.push","publisherId":"tfs","message":{"text":"commit pushed","html":"commit pushed"},"detailedMessage":{"text":"pushed a commit to git-test"},"resource":{"commits":[{"commitId":"` + commit + `","author":{"name":"fleet","email":"[email protected]","date":"2024-01-05T10:16:56Z"},"committer":{"name":"fleet","email":"[email protected]","date":"2024-01-05T10:16:56Z"},"comment":"test commit","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/commits/f00c3a181697bb3829a6462e931c7456bbed557b"}],"refUpdates":[{"name":"refs/heads/main","oldObjectId":"135f8a827edae980466f72eef385881bb4e158d8","newObjectId":"` + commit + `"}],"repository":{"id":"xxx","name":"git-test","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx","project":{"id":"xxx","name":"git-test","url":"https://dev.azure.com/fleet/_apis/projects/xxx","state":"wellFormed","visibility":"unchanged","lastUpdateTime":"0001-01-01T00:00:00"},"defaultBranch":"refs/heads/main","remoteUrl":"` + repoURL + `"},"pushedBy":{"displayName":"Fleet","url":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx","_links":{"avatar":{"href":"https://dev.azure.com/fleet/_apis/GraphProfile/MemberAvatars/msa.xxxx"}},"id":"xxx","uniqueName":"[email protected]","imageUrl":"https://dev.azure.com/fleet/_api/_common/identityImage?id=xxx","descriptor":"xxxx"},"pushId":22,"date":"2024-01-05T10:17:18.735088Z","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22","_links":{"self":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22"},"repository":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx"},"commits":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22/commits"},"pusher":{"href":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx"},"refs":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx/refs/heads/main"}}},"resourceVersion":"1.0","resourceContainers":{"collection":{"id":"xxx","baseUrl":"https://dev.azure.com/fleet/"},"account":{"id":"ec365173-fce3-4dfc-8fc2-950f0b5728b1","baseUrl":"https://dev.azure.com/fleet/"},"project":{"id":"xxx","baseUrl":"https://dev.azure.com/fleet/"}},"createdDate":"2024-01-05T10:17:26.0098694Z"}`)
bodyReader := bytes.NewReader(jsonBody)
req, err := http.NewRequest(http.MethodPost, repoURL, bodyReader)
if err != nil {
t.Errorf("unexpected err %v", err)
}
h := http.Header{}
h.Add("X-Vss-Activityid", "xxx")
req.Header = h

expectedStatusUpdate := gitjob.DeepCopy()
expectedStatusUpdate.Status.Commit = commit
mockGitjobCache.EXPECT().List("", labels.Everything()).Return(gitjobs, nil)
mockGitjob.EXPECT().Cache().Return(mockGitjobCache)
mockGitjob.EXPECT().UpdateStatus(expectedStatusUpdate).Return(gitjob, nil)
mockGitjob.EXPECT().Update(gitjob)

w.ServeHTTP(&responseWriter{}, req)
}

type responseWriter struct{}

func (r *responseWriter) Header() http.Header {
return http.Header{}
}
func (r *responseWriter) Write([]byte) (int, error) {
return 0, nil
}

func (r *responseWriter) WriteHeader(statusCode int) {}

0 comments on commit cb9a0ea

Please sign in to comment.