Skip to content

Commit

Permalink
Add azure webhooks support
Browse files Browse the repository at this point in the history
  • Loading branch information
raulcabello committed Jan 17, 2024
1 parent bd27fe7 commit 403bb77
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/go-git/go-git/v5 v5.11.0
github.com/go-logr/logr v1.4.1
github.com/go-playground/webhooks/v6 v6.3.0
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/google/go-cmp v0.6.0
github.com/gorilla/mux v1.8.1
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
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
}
24 changes: 24 additions & 0 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"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"

Expand Down Expand Up @@ -39,6 +42,8 @@ const (
bitbucketKey = "bitbucket"
bitbucketServerKey = "bitbucket-server"
gogsKey = "gogs"
azureUsername = "azure-username"
azurePassword = "azure-password"

branchRefPrefix = "refs/heads/"
tagRefPrefix = "refs/tags/"
Expand All @@ -53,6 +58,7 @@ type Webhook struct {
bitbucketServer *bitbucketserver.Webhook
gogs *gogs.Webhook
log logr.Logger
azureDevops *azuredevops.Webhook
}

func New(namespace string, client client.Client) (*Webhook, error) {
Expand Down Expand Up @@ -92,6 +98,10 @@ func (w *Webhook) initGitProviders() error {
if err != nil {
return err
}
w.azureDevops, err = azuredevops.New()
if err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -131,6 +141,11 @@ func (w *Webhook) onSecretChange(obj interface{}) error {
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
}
Expand All @@ -153,6 +168,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 @@ -212,6 +229,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
}
}

var gitJobList v1.GitJobList
Expand Down
67 changes: 67 additions & 0 deletions pkg/webhook/webhook_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
package webhook

import (
"bytes"
"context"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"

v1 "github.com/rancher/gitjob/pkg/apis/gitjob.cattle.io/v1"
"github.com/rancher/gitjob/pkg/webhook/azuredevops"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cfake "sigs.k8s.io/controller-runtime/pkg/client/fake"

"net/http"
"testing"

"gotest.tools/assert"
Expand Down Expand Up @@ -31,3 +43,58 @@ 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"
gitjob := &v1.GitJob{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: v1.GitJobSpec{
Git: v1.GitInfo{
Repo: repoURL,
Branch: "main",
},
},
}
scheme := runtime.NewScheme()
err := v1.AddToScheme(scheme)
if err != nil {
t.Errorf("unexpected error %v", err)
}
client := cfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gitjob).WithStatusSubresource(gitjob).Build()
w := &Webhook{client: client}
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":"fleet@suse.com","date":"2024-01-05T10:16:56Z"},"committer":{"name":"fleet","email":"fleet@suse.com","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":"fleet@suse.com","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

w.ServeHTTP(&responseWriter{}, req)

updatedGitJob := &v1.GitJob{}
err = client.Get(context.TODO(), types.NamespacedName{Name: gitjob.Name, Namespace: gitjob.Namespace}, updatedGitJob)
if err != nil {
t.Errorf("unexpected err %v", err)
}
if updatedGitJob.Status.Commit != commit {
t.Errorf("expected commit %v, but got %v", commit, updatedGitJob.Status.Commit)
}
}

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 403bb77

Please sign in to comment.