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

added support for azure devops #1654

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
58 changes: 58 additions & 0 deletions docs/content/docs/install/azure_devops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Azure Devops
weight: 17
---
# Use Pipelines-as-Code with Azure Devops

Pipelines-As-Code supports on [Azure Devops](https://azure.microsoft.com/en-us/products/devops) through a webhook.

Follow the Pipelines-As-Code [installation](/docs/install/installation) according to your Kubernetes cluster.

* You will have to generate a personal token as the manager of the Project,
follow the steps here:

<https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows>

The token will need to have atleast `Read, write, & manage` permissions under `code`.

You may want to note somewhere the generated token, or otherwise you will have to
recreate it.

* Create a Webhook on the repository following this guide :
<https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops>

Provide the header value of service hook based on required event type

| Event Type | Header Value |
| ----------|---------|
| Code Pushed | X-Azure-DevOps-EventType:git.push |
| Pull request created | X-Azure-DevOps-EventType:git.pullrequest.created|
| Pull request updated | X-Azure-DevOps-EventType:git.pullrequest.updated|
| Pull request commented on| X-Azure-DevOps-EventType:git.pullrequest.comment|

* Create a secret with personal token in the `target-namespace`

```shell
kubectl -n target-namespace create secret generic webhook-config \
--from-literal provider.token="TOKEN_AS_GENERATED_PREVIOUSLY" \
```

* And finally create Repository CRD with the secret field referencing it.

* Here is an example of a Repository CRD :

```yaml
---
apiVersion: "pipelinesascode.tekton.dev/v1alpha1"
kind: Repository
metadata:
name: my-repo
namespace: target-namespace
spec:
url: 'https://dev.azure.com/YOUR_ORG_NAME/YOUR_PROJ_NAME/_git/YOUR_REPO_NAME'
git_provider:
secret:
name: "webhook-config"
# Set this if you have a different key in your secret
# key: "provider.token"
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ require (
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down Expand Up @@ -1019,6 +1020,10 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s=
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
Expand Down
7 changes: 7 additions & 0 deletions pkg/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/version"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/azuredevops"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketserver"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/gitea"
Expand Down Expand Up @@ -249,6 +250,12 @@ func (l listener) detectProvider(req *http.Request, reqBody string) (provider.In
return l.processRes(processReq, bitCloud, logger, reason, err)
}

ado := &azuredevops.Provider{}
isAdo, processReq, logger, reason, err := ado.Detect(req, reqBody, &log)
if isAdo {
return l.processRes(processReq, ado, logger, reason, err)
}

return l.processRes(false, nil, logger, "", fmt.Errorf("no supported Git provider has been detected"))
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/pipelinesascode/keys/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const (
MaxKeepRuns = pipelinesascode.GroupName + "/max-keep-runs"
LogURL = pipelinesascode.GroupName + "/log-url"
ExecutionOrder = pipelinesascode.GroupName + "/execution-order"
ProjectID = pipelinesascode.GroupName + "/project-id"
RepositoryID = pipelinesascode.GroupName + "/repository-id"
// PublicGithubAPIURL default is "https://api.github.com" but it can be overridden by X-GitHub-Enterprise-Host header.
PublicGithubAPIURL = "https://api.github.com"
// InstallationURL gives us the Installation ID for the GitHub Application.
Expand Down
11 changes: 11 additions & 0 deletions pkg/kubeinteraction/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRu
keys.GitProvider: providerConfig.Name,
keys.State: StateStarted,
keys.ControllerInfo: fmt.Sprintf(`{"name":"%s","configmap":"%s","secret":"%s"}`, paramsinfo.Controller.Name, paramsinfo.Controller.Configmap, paramsinfo.Controller.Secret),
keys.RepositoryID: event.RepositoryID,
keys.ProjectID: event.ProjectID,
}

if event.PullRequestNumber != 0 {
Expand All @@ -80,6 +82,15 @@ func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRu
annotations[keys.TargetProjectID] = strconv.Itoa(event.TargetProjectID)
}

// Azure devops

if event.RepositoryID != "" {
annotations[keys.RepositoryID] = event.RepositoryID
}
if event.ProjectID != "" {
annotations[keys.ProjectID] = event.ProjectID
}

for k, v := range labels {
pipelineRun.Labels[k] = v
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/params/info/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ type Event struct {
// Gitlab
SourceProjectID int
TargetProjectID int

// AzureDevops
RepositoryID string
ProjectID string
}

type State struct {
Expand Down
98 changes: 98 additions & 0 deletions pkg/provider/azuredevops/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package azuredevops

import (
"context"
"fmt"
"strings"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/openshift-pipelines/pipelines-as-code/pkg/acl"
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
)

// ToDo: implement this function.
func (v *Provider) CheckPolicyAllowing(context.Context, *info.Event, []string) (bool, string) {
return false, ""
}

func (v *Provider) IsAllowed(ctx context.Context, event *info.Event) (bool, error) {

allowed, _ := v.checkMembership(ctx, event)
if allowed {
return true, nil
}

// Try to parse the comment from an owner who has issues a /ok-to-test
ownerAllowed, err := v.aclAllowedOkToTestFromAnOwner(ctx, event)
if err != nil {
return false, err
}
if ownerAllowed {
return true, nil
}
return false, nil
}

func (v *Provider) aclAllowedOkToTestFromAnOwner(ctx context.Context, event *info.Event) (bool, error) {
if event.EventType == opscomments.OkToTestCommentEventType.String() {
allowed, _ := v.checkMembership(ctx, event)
if allowed {
return true, nil
}
}
return false, nil
}

func (v *Provider) checkMembership(ctx context.Context, event *info.Event) (bool, error) {
teams, err := v.CoreClient.GetTeams(ctx, core.GetTeamsArgs{
ProjectId: &event.ProjectID,
})

if err != nil {
return false, err
}

// Check if the PR author is a member of any team
for _, team := range *teams {
if team.Id == nil {
continue
}
teamIdStr := team.Id.String()
members, err := v.CoreClient.GetTeamMembersWithExtendedProperties(ctx, core.GetTeamMembersWithExtendedPropertiesArgs{
ProjectId: &event.ProjectID,
TeamId: &teamIdStr,
})
if err != nil {
continue
}

for _, member := range *members {
if *member.Identity.Id == event.Sender {
return true, nil
}
}
}
return v.IsAllowedOwnersFile(ctx, event)
}

func (v *Provider) IsAllowedOwnersFile(ctx context.Context, event *info.Event) (bool, error) {
ownerContent, err := v.getFileFromDefaultBranch(ctx, "OWNERS", event)
if err != nil {
if strings.Contains(err.Error(), "cannot find") {
// no owner file, skipping
return false, nil
}
return false, err
}

return acl.UserInOwnerFile(ownerContent, event.Sender)
}

func (v *Provider) getFileFromDefaultBranch(ctx context.Context, path string, runevent *info.Event) (string, error) {
owner, err := v.GetFileInsideRepo(ctx, runevent, path, runevent.DefaultBranch)
if err != nil {
return "", fmt.Errorf("cannot find %s inside the %s branch: %w", path, runevent.DefaultBranch, err)
}
return owner, err
}
79 changes: 79 additions & 0 deletions pkg/provider/azuredevops/acl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package azuredevops

import (
"testing"

"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/azuredevops/test"
"go.uber.org/zap"
"gotest.tools/v3/assert"
rtesting "knative.dev/pkg/reconciler/testing"
)

func TestIsAllowedAzureDevOps(t *testing.T) {

type fields struct {
teamMembers map[string][]string
}

tests := []struct {
name string
event *info.Event
fields fields
isAllowed bool
wantErrSubstr string
}{
{
name: "allowed/user is team member",
event: &info.Event{
Sender: "user123",
ProjectID: "project1",
},
fields: fields{
teamMembers: map[string][]string{
"00000000-0000-0000-0000-000000000000": {"user123", "user456"},
},
},
isAllowed: true,
},
{
name: "disallowed/user not a team member",
event: &info.Event{
Sender: "user999",
ProjectID: "project1",
},
fields: fields{
teamMembers: map[string][]string{
"00000000-0000-0000-0000-000000000000": {"user123", "user456"},
},
},
isAllowed: false,
},
}

mockGit, mockCore, _, _, tearDown := test.Setup()

defer tearDown()

logger := zap.NewExample().Sugar()
defer logger.Sync()
ctx, _ := rtesting.SetupFakeContext(t)
provider := Provider{
GitClient: mockGit,
CoreClient: mockCore,
Logger: logger,
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockCore.TeamMembers = tt.fields.teamMembers
got, err := provider.IsAllowed(ctx, tt.event)
if tt.wantErrSubstr != "" {
assert.ErrorContains(t, err, tt.wantErrSubstr)
return
}
assert.NilError(t, err)
assert.Equal(t, tt.isAllowed, got, "Provider.IsAllowed() = %v, want %v", got, tt.isAllowed)
})
}
}
Loading
Loading