Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add azure webhook #412

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {}