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

(feat) add azure webhook parsing, creation deletion & list #163

Merged
merged 1 commit into from
Mar 30, 2022
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
(feat) add azure webhook parsing, creation deletion & list
  • Loading branch information
TP Honey committed Mar 30, 2022
commit 7e9ed2b3a1ede9797d988325cb1dcf09a2b7440e
73 changes: 73 additions & 0 deletions scm/driver/azure/integration/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,76 @@ func TestListRepos(t *testing.T) {
t.Errorf("List should have at least 1 repo %d", len(references))
}
}

func TestListHooks(t *testing.T) {
if token == "" {
t.Skip("Skipping, Acceptance test")
}
client = azure.NewDefault(organization, project)
client.Client = &http.Client{
Transport: &transport.Custom{
Before: func(r *http.Request) {
r.Header.Set("Authorization", fmt.Sprintf("Basic %s", token))
},
},
}
hooks, response, listerr := client.Repositories.ListHooks(context.Background(), repoID, scm.ListOptions{})
if listerr != nil {
t.Errorf("List got an error %v", listerr)
}
if response.Status != http.StatusOK {
t.Errorf("List did not get a 200 back %v", response.Status)
}
if len(hooks) < 1 {
t.Errorf("List should have at least 1 hook %d", len(hooks))
}
}

func TestCreateDeleteHooks(t *testing.T) {
if token == "" {
t.Skip("Skipping, Acceptance test")
}
client = azure.NewDefault(organization, project)
client.Client = &http.Client{
Transport: &transport.Custom{
Before: func(r *http.Request) {
r.Header.Set("Authorization", fmt.Sprintf("Basic %s", token))
},
},
}
originalHooks, _, _ := client.Repositories.ListHooks(context.Background(), repoID, scm.ListOptions{})
// create a new hook
inputHook := &scm.HookInput{
Name: "web",
NativeEvents: []string{"git.push"},
Target: "http:https://www.example.com/webhook",
}
outHook, createResponse, createErr := client.Repositories.CreateHook(context.Background(), repoID, inputHook)
if createErr != nil {
t.Errorf("Create got an error %v", createErr)
}
if createResponse.Status != http.StatusOK {
t.Errorf("Create did not get a 200 back %v", createResponse.Status)
}
if len(outHook.Events) != 1 {
t.Errorf("New hook has one event %d", len(outHook.Events))
}
// get the hooks again, and make sure the new hook is there
afterCreate, _, _ := client.Repositories.ListHooks(context.Background(), repoID, scm.ListOptions{})
if len(afterCreate) != len(originalHooks)+1 {
t.Errorf("After create, the number of hooks is not correct %d. It should be %d", len(afterCreate), len(originalHooks)+1)
}
// delete the hook we created
deleteResponse, deleteErr := client.Repositories.DeleteHook(context.Background(), repoID, outHook.ID)
if deleteErr != nil {
t.Errorf("Delete got an error %v", deleteErr)
}
if deleteResponse.Status != http.StatusNoContent {
t.Errorf("Delete did not get a 204 back, got %v", deleteResponse.Status)
}
// get the hooks again, and make sure the new hook is gone
afterDelete, _, _ := client.Repositories.ListHooks(context.Background(), repoID, scm.ListOptions{})
if len(afterDelete) != len(originalHooks) {
t.Errorf("After Delete, the number of hooks is not correct %d. It should be %d", len(afterDelete), len(originalHooks))
}
}
152 changes: 149 additions & 3 deletions scm/driver/azure/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ func (s *RepositoryService) List(ctx context.Context, opts scm.ListOptions) ([]*

// ListHooks returns a list or repository hooks.
func (s *RepositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
// https://docs.microsoft.com/en-us/rest/api/azure/devops/hooks/subscriptions/list?view=azure-devops-rest-6.0
endpoint := fmt.Sprintf("%s/_apis/hooks/subscriptions?api-version=6.0", s.client.owner)
out := new(subscriptions)
res, err := s.client.do(ctx, "GET", endpoint, nil, &out)
return convertHookList(out.Value, repo), res, err
}

// ListStatus returns a list of commit statuses.
Expand All @@ -54,7 +58,37 @@ func (s *RepositoryService) ListStatus(ctx context.Context, repo, ref string, op

// CreateHook creates a new repository webhook.
func (s *RepositoryService) CreateHook(ctx context.Context, repo string, input *scm.HookInput) (*scm.Hook, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
// https://docs.microsoft.com/en-us/rest/api/azure/devops/hooks/subscriptions/create?view=azure-devops-rest-6.0
endpoint := fmt.Sprintf("%s/_apis/hooks/subscriptions?api-version=6.0", s.client.owner)
in := new(subscription)
in.Status = "enabled"
in.PublisherID = "tfs"
in.ResourceVersion = "1.0"
in.ConsumerID = "webHooks"
in.ConsumerActionID = "httpRequest"
// we do not support scm hookevents, only native events
if input.NativeEvents == nil {
return nil, nil, fmt.Errorf("CreateHook, You must pass at least one native event")
}
if len(input.NativeEvents) > 1 {
return nil, nil, fmt.Errorf("CreateHook, Azure only allows the creation of a single hook at a time %v", input.NativeEvents)
}
in.EventType = input.NativeEvents[0]
// publisher
projectID, projErr := s.getProjectIDFromProjectName(ctx, s.client.project)
if projErr != nil {
return nil, nil, fmt.Errorf("CreateHook was unable to look up the project's projectID, %s", projErr)
}
in.PublisherInputs.ProjectID = projectID
in.PublisherInputs.Repository = repo
// consumer
in.ConsumerInputs.URL = input.Target
if input.SkipVerify {
in.ConsumerInputs.AcceptUntrustedCerts = "enabled"
}
out := new(subscription)
res, err := s.client.do(ctx, "POST", endpoint, in, out)
return convertHook(out), res, err
}

// CreateStatus creates a new commit status.
Expand All @@ -74,7 +108,38 @@ func (s *RepositoryService) UpdateHook(ctx context.Context, repo, id string, inp

// DeleteHook deletes a repository webhook.
func (s *RepositoryService) DeleteHook(ctx context.Context, repo, id string) (*scm.Response, error) {
return nil, scm.ErrNotSupported
// https://docs.microsoft.com/en-us/rest/api/azure/devops/hooks/subscriptions/delete?view=azure-devops-rest-6.0
endpoint := fmt.Sprintf("%s/_apis/hooks/subscriptions/%s?api-version=6.0", s.client.owner, id)
return s.client.do(ctx, "DELETE", endpoint, nil, nil)
}

// helper function to return the projectID from the project name
func (s *RepositoryService) getProjectIDFromProjectName(ctx context.Context, projectName string) (string, error) {
// https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/list?view=azure-devops-rest-6.0
endpoint := fmt.Sprintf("%s/_apis/projects?api-version=6.0", s.client.owner)
type projects struct {
Count int64 `json:"count"`
Value []struct {
Description string `json:"description"`
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
URL string `json:"url"`
} `json:"value"`
}

out := new(projects)
response, err := s.client.do(ctx, "GET", endpoint, nil, &out)
if err != nil {
fmt.Println(response)
return "", fmt.Errorf("failed to list projects: %s", err)
}
for _, v := range out.Value {
if v.Name == projectName {
return v.ID, nil
}
}
return "", fmt.Errorf("failed to find project id for %s", projectName)
}

type repositories struct {
Expand All @@ -96,6 +161,64 @@ type repository struct {
URL string `json:"url"`
}

type subscriptions struct {
Count int64 `json:"count"`
Value []*subscription `json:"value"`
}

type subscription struct {
ActionDescription string `json:"actionDescription"`
ConsumerActionID string `json:"consumerActionId"`
ConsumerID string `json:"consumerId"`
ConsumerInputs struct {
AccountName string `json:"accountName,omitempty"`
AcceptUntrustedCerts string `json:"acceptUntrustedCerts,omitempty"`
AddToTop string `json:"addToTop,omitempty"`
APIToken string `json:"apiToken,omitempty"`
BoardID string `json:"boardId,omitempty"`
BuildName string `json:"buildName,omitempty"`
BuildParameterized string `json:"buildParameterized,omitempty"`
FeedID string `json:"feedId,omitempty"`
ListID string `json:"listId,omitempty"`
PackageSourceID string `json:"packageSourceId,omitempty"`
Password string `json:"password,omitempty"`
ServerBaseURL string `json:"serverBaseUrl,omitempty"`
URL string `json:"url,omitempty"`
UserToken string `json:"userToken,omitempty"`
Username string `json:"username,omitempty"`
} `json:"consumerInputs"`
CreatedBy struct {
ID string `json:"id"`
} `json:"createdBy"`
CreatedDate string `json:"createdDate"`
EventDescription string `json:"eventDescription"`
EventType string `json:"eventType"`
ID string `json:"id"`
ModifiedBy struct {
ID string `json:"id"`
} `json:"modifiedBy"`
ModifiedDate string `json:"modifiedDate"`
ProbationRetries int64 `json:"probationRetries"`
PublisherID string `json:"publisherId"`
PublisherInputs struct {
AreaPath string `json:"areaPath,omitempty"`
Branch string `json:"branch,omitempty"`
BuildStatus string `json:"buildStatus,omitempty"`
ChangedFields string `json:"changedFields,omitempty"`
CommentPattern string `json:"commentPattern,omitempty"`
DefinitionName string `json:"definitionName,omitempty"`
HostID string `json:"hostId,omitempty"`
Path string `json:"path,omitempty"`
ProjectID string `json:"projectId,omitempty"`
Repository string `json:"repository,omitempty"`
TfsSubscriptionID string `json:"tfsSubscriptionId,omitempty"`
WorkItemType string `json:"workItemType,omitempty"`
} `json:"publisherInputs"`
ResourceVersion string `json:"resourceVersion"`
Status string `json:"status"`
URL string `json:"url"`
}

// helper function to convert from the gogs repository list to
// the common repository structure.
func convertRepositoryList(from *repositories) []*scm.Repository {
Expand All @@ -116,3 +239,26 @@ func convertRepository(from *repository) *scm.Repository {
Branch: from.DefaultBranch,
}
}

func convertHookList(from []*subscription, repositoryFilter string) []*scm.Hook {
to := []*scm.Hook{}
for _, v := range from {
if repositoryFilter != "" && repositoryFilter == v.PublisherInputs.Repository {
to = append(to, convertHook(v))
}
}
return to
}

func convertHook(from *subscription) *scm.Hook {
returnVal := &scm.Hook{
ID: from.ID,

Active: from.Status == "enabled",
Target: from.ConsumerInputs.URL,
Events: []string{from.EventType},
SkipVerify: from.ConsumerInputs.AcceptUntrustedCerts == "true",
}

return returnVal
}
59 changes: 59 additions & 0 deletions scm/driver/azure/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,62 @@ func TestRepositoryList(t *testing.T) {
t.Log(diff)
}
}

func TestRepositoryHookCreate(t *testing.T) {
defer gock.Off()

gock.New("https:/dev.azure.com/").
Get("/ORG/_apis/projects").
Reply(201).
Type("application/json").
File("testdata/projects.json")

gock.New("https:/dev.azure.com/").
Post("/ORG/_apis/hooks/subscriptions").
Reply(201).
Type("application/json").
File("testdata/hook.json")

in := &scm.HookInput{
Name: "web",
NativeEvents: []string{"git.push"},
Target: "http:https://www.example.com/webhook",
}

client := NewDefault("ORG", "test_project")
got, _, err := client.Repositories.CreateHook(context.Background(), "test_project", in)
if err != nil {
t.Error(err)
return
}

want := new(scm.Hook)
raw, _ := ioutil.ReadFile("testdata/hook.json.golden")
_ = json.Unmarshal(raw, want)

if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}

func TestRepositoryHookDelete(t *testing.T) {
defer gock.Off()

gock.New("https:/dev.azure.com/").
Delete("/ORG/_apis/hooks/subscriptions").
Reply(204).
Type("application/json")

client := NewDefault("ORG", "PROJ")
res, err := client.Repositories.DeleteHook(context.Background(), "", "test-project")
if err != nil {
t.Error(err)
return
}

if got, want := res.Status, 204; got != want {
t.Errorf("Want response status %d, got %d", want, got)
}

}
58 changes: 58 additions & 0 deletions scm/driver/azure/testdata/hook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"id": "d455cb11-20a0-4b15-b546-7e9fb9973cc6",
"url": "https://dev.azure.com/tphoney/_apis/hooks/subscriptions/d455cb11-20a0-4b15-b546-7e9fb9973cc6",
"status": "enabled",
"publisherId": "tfs",
"eventType": "git.pullrequest.created",
"subscriber": null,
"resourceVersion": "1.0",
"eventDescription": "Repository test_repo2",
"consumerId": "webHooks",
"consumerActionId": "httpRequest",
"actionDescription": "To host www.bla.com",
"probationRetries": 1,
"createdBy": {
"displayName": "tp",
"id": "3ff4a20f-306e-677e-8a01-57f35e71f109",
"uniqueName": "[email protected]",
"descriptor": "msa.M2ZmNGEyMGYtMzA2ZS03NzdlLThhMDEtNTdmMzVlNzFmMTA5"
},
"createdDate": "2022-03-25T13:28:12.39Z",
"modifiedBy": {
"displayName": "tp",
"id": "3ff4a20f-306e-677e-8a01-57f35e71f109",
"uniqueName": "[email protected]",
"descriptor": "msa.M2ZmNGEyMGYtMzA2ZS03NzdlLThhMDEtNTdmMzVlNzFmMTA5"
},
"modifiedDate": "2022-03-29T10:39:13.813Z",
"lastProbationRetryDate": "2022-03-28T10:44:51.093Z",
"publisherInputs": {
"branch": "",
"projectId": "d350c9c0-7749-4ff8-a78f-f9c1f0e56729",
"pullrequestCreatedBy": "",
"pullrequestReviewersContains": "",
"repository": "fde2d21f-13b9-4864-a995-83329045289a",
"tfsSubscriptionId": "4ce8d6c4-f655-418d-8eb6-9462dd01ff39"
},
"consumerInputs": {
"acceptUntrustedCerts": "true",
"url": "http:https://www.bla.com"
},
"_links": {
"self": {
"href": "https://dev.azure.com/tphoney/_apis/hooks/subscriptions/d455cb11-20a0-4b15-b546-7e9fb9973cc6"
},
"consumer": {
"href": "https://dev.azure.com/tphoney/_apis/hooks/consumers/webHooks"
},
"actions": {
"href": "https://dev.azure.com/tphoney/_apis/hooks/consumers/webHooks/actions"
},
"notifications": {
"href": "https://dev.azure.com/tphoney/_apis/hooks/subscriptions/d455cb11-20a0-4b15-b546-7e9fb9973cc6/notifications"
},
"publisher": {
"href": "https://dev.azure.com/tphoney/_apis/hooks/publishers/tfs"
}
}
}
10 changes: 10 additions & 0 deletions scm/driver/azure/testdata/hook.json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"ID": "d455cb11-20a0-4b15-b546-7e9fb9973cc6",
"Name": "",
"Target": "http:https://www.bla.com",
"Events": [
"git.pullrequest.created"
],
"Active": true,
"SkipVerify": true
}
Loading