-
Notifications
You must be signed in to change notification settings - Fork 30
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 devops webhook support #401
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,20 @@ | ||
package webhook | ||
|
||
//go:generate mockgen --build_flags=--mod=mod -destination=../../internal/mocks/gitjob_cache_mock.go -package=mocks github.com/rancher/gitjob/pkg/generated/controllers/gitjob.cattle.io/v1 GitJobCache | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/rancher/gitjob/pkg/webhook/azuredevops" | ||
|
||
"github.com/golang/mock/gomock" | ||
"github.com/rancher/gitjob/internal/mocks" | ||
v1 "github.com/rancher/gitjob/pkg/apis/gitjob.cattle.io/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/labels" | ||
|
||
"gotest.tools/assert" | ||
) | ||
|
||
|
@@ -31,3 +43,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" | ||
ctlr := gomock.NewController(t) | ||
mockGitjob := mocks.NewMockGitJobController(ctlr) | ||
mockGitjobCache := mocks.NewMockGitJobCache(ctlr) | ||
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"}`) | ||
raulcabello marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think it could make sense to copy the corresponding test file over as well, just to be on the safe side. How do you see it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would also need to copy all the testdata files.. https://github.com/go-playground/webhooks/blob/master/azuredevops/azuredevops_test.go#L63
I would avoid copying all those files. Should we leave it without the test or maybe we can just transfer my fork to the fleet org and use the fork instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's leave it without the test for now; in the meantime, I'll be watching your PR so that we can switch back to using upstream ASAP. Thanks for clarifying :)