Skip to content

Azure devops #216

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

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3a60af6
add intial setup & get list of projects api
nellyk Jan 17, 2023
7c0a256
add intial setup & get list of projects api
nellyk Jan 17, 2023
08d5cc2
adding a few tests and apis
nellyk Jan 24, 2023
dacd214
add get repository for project method for azure devops
nellyk Jan 24, 2023
8de69f7
add get repository for project method for azure devops
nellyk Jan 27, 2023
addba5d
address review comments
nellyk Feb 2, 2023
a281f36
remove space on auth file
nellyk Feb 3, 2023
7ebb4c2
add support for Tree API
nellyk Feb 9, 2023
4e797ed
add create branch azuredevops
nellyk Feb 10, 2023
94bd68a
Update azuredevops/client_repository_tree.go
nellyk Feb 13, 2023
b9a6ffd
add intial setup & get list of projects api
nellyk Jan 17, 2023
3d7c068
add intial setup & get list of projects api
nellyk Jan 17, 2023
8f846ba
adding a few tests and apis
nellyk Jan 24, 2023
25257c1
add get repository for project method for azure devops
nellyk Jan 24, 2023
a7ffc7c
add get repository for project method for azure devops
nellyk Jan 27, 2023
748da57
address review comments
nellyk Feb 2, 2023
44c99a9
remove space on auth file
nellyk Feb 3, 2023
73d2cad
add support for Tree API
nellyk Feb 9, 2023
da06ba2
add create branch azuredevops
nellyk Feb 10, 2023
218aa50
Add ability to create commits azure devops
nellyk Feb 14, 2023
f1be3fe
Add list commit azure devops
nellyk Feb 14, 2023
65d258b
Add ability to create pull request
nellyk Feb 15, 2023
b09453d
review comments
nellyk Feb 21, 2023
aaad427
Update azuredevops/client_repository_commit.go
nellyk Feb 23, 2023
e80740b
review comments
nellyk Feb 24, 2023
f957e4a
review comments update commits function
nellyk Mar 6, 2023
577f59e
commit review comments
nellyk Mar 7, 2023
97c8e28
adding e2e tests for org repositories
souleb Feb 23, 2023
2d93283
added repo list and fixing integration tests
nellyk Mar 1, 2023
e3b554c
Add ability to create repo
nellyk Mar 1, 2023
8a3c83a
intergration tests
nellyk Mar 6, 2023
ab2b039
fix integration tests
nellyk Mar 7, 2023
8aad5eb
remove space
nellyk Mar 22, 2023
df7e404
Update azuredevops/client_repositories.go
nellyk Mar 22, 2023
545900b
update review comments
nellyk Mar 30, 2023
e2c87e1
update review comments
nellyk Mar 30, 2023
2b7d1b1
Update azuredevops/util.go
nellyk Mar 30, 2023
a9c2c92
Update azuredevops/integration_suite_test.go
nellyk Mar 30, 2023
97994a4
update review comments
nellyk Mar 30, 2023
0ee0913
Add pull request methods
nellyk Mar 31, 2023
c5b9980
review comments - add pr
nellyk Apr 4, 2023
975a68c
remove panic comments
nellyk Apr 4, 2023
2e3b111
adding missing DeployToken method
souleb Apr 6, 2023
2256f08
adding azure env var to workflow
souleb Apr 6, 2023
f3bf893
Adding azure devops projects tests
souleb Apr 28, 2023
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
34 changes: 34 additions & 0 deletions .github/workflows/e2e-azuredevops.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: e2e-azuredevops

on:
workflow_dispatch:
push:
branches: ["*"]
tags-ignore: ["*"]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Restore Go cache
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
with:
go-version: 1.18.x
- name: Run tests
run: |
[ -n "${{ secrets.AZURE_TOKEN }}" ] && export AZURE_TOKEN=${{ secrets.AZURE_TOKEN }} || echo "using default AZURE_TOKEN"
[ -n "${{ secrets.AZURE_DOMAIN }}" ] && export AZURE_DOMAIN=${{ secrets.AZURE_DOMAIN }} || echo "using default AZURE_DOMAIN"
make test-e2e-azuredevops
- name: Upload coverage to Codecov
uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
with:
files: ./coverage.txt
12 changes: 12 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ make test-e2e-stash
| Test organization | go-git-provider-testing | `GIT_PROVIDER_ORGANIZATION` |
| Test team | fluxcd-test-team | `STASH_TEST_TEAM_NAME` |


### Azure DevOps
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Azure DevOps
#### Azure DevOps


All tests are run against dev.azure.com. Adjust the following variables to your needs:

| Setting | Default value | Environment variable |
| ----------------- | ---------------------------- | --------------------------- |
| Domain | dev.azure.com | `AZURE_DOMAIN` |
| Access token | read from `/tmp/azure.token` | `AZURE_TOKEN` |
| Test organization | flux | `GIT_PROVIDER_ORGANIZATION` |
| Test team | fluxcd-test-team | `AZURE_TEST_TEAM_NAME` |

## Continuous Integration

The e2e test suite runs in GitHub Actions on each commit to the main branch and on branches pushed to the repository, i.e. on PRs created from people with write access.
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ test-e2e-gitlab: tidy fmt vet
test-e2e-stash: tidy fmt vet
go test ${TEST_FLAGS} -race -coverprofile=coverage.txt -covermode=atomic -tags=e2e ./stash/...

test-e2e-azuredevops: tidy fmt vet
go test ${TEST_FLAGS} -race -coverprofile=coverage.txt -covermode=atomic -tags=e2e ./azuredevops/...

71 changes: 71 additions & 0 deletions azuredevops/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright 2023 The Flux CD contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azuredevops

import (
"context"
"fmt"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"

"net/url"
)

// NewClient creates a new Client instance for Azure Devops API endpoints.
// The client accepts a personal token used which is used to authenticate and a context as an argument,
// Variadic parameters gitprovider.ClientOption are used to pass additional options to the gitprovider.Client.
func NewClient(personalAccessToken string, ctx context.Context, optFns ...gitprovider.ClientOption) (gitprovider.Client, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this func in a file named auth.go? Wouldn't something like client.go be more appropriate?

opts, err := gitprovider.MakeClientOptions(optFns...)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should entertain the habit of wrapping errors and add context:

Suggested change
return nil, err
return nil, fmt.Errorf("failed making client options: %w", err)

}

if opts.Domain == nil {
return nil, fmt.Errorf("domain is required")
}

domain := *opts.Domain
u, err := url.Parse(domain)
if err != nil {
return nil, fmt.Errorf("invalid domain: %w", err)
}
if u.Scheme == "" || u.Scheme == "http" {
domain = fmt.Sprintf("https://%s%s", u.Host, u.Path)
}

connection := azuredevops.NewPatConnection(domain, personalAccessToken)
// coreClient provides access to Azure Devops organization,projects and teams
coreClient, err := core.NewClient(ctx, connection)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, err
return nil, fmt.Errorf("failed creating client: %w", err)

}
// gitClient provides access to the Azure Devops Git repositories the files,trees,commits and refs
gitClient, err := git.NewClient(ctx, connection)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, err
return nil, fmt.Errorf("failed creating git client: %w", err)

}

destructiveActions := false
if opts.EnableDestructiveAPICalls != nil {
destructiveActions = *opts.EnableDestructiveAPICalls
}

return newClient(coreClient, gitClient, domain, destructiveActions), nil
}
63 changes: 63 additions & 0 deletions azuredevops/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2023 The Flux CD contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package azuredevops

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/fluxcd/go-git-providers/gitprovider"
)

func Test_DomainVariations(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other opts we want to test?

tests := []struct {
name string
opts gitprovider.ClientOption
want string
expectedErrs []error
}{
{
name: "custom domain without protocol",
opts: gitprovider.WithDomain("dev.azure.com/test-demo"),
want: "https://dev.azure.com/test-demo",
},
{
name: "custom domain with https protocol",
opts: gitprovider.WithDomain("https://dev.azure.com/test-demo"),
want: "https://dev.azure.com/test-demo",
},
{
name: "custom domain with http protocol",
opts: gitprovider.WithDomain("http://dev.azure.com/test-demo"),
want: "https://dev.azure.com/test-demo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
c2, err := NewClient("personalAccessToken", ctx, tt.opts)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tt.want, c2.SupportedDomain()); diff != "" {
t.Errorf("New Azure client returned domain (want -> got): %s", diff)
}
})
}
}
108 changes: 108 additions & 0 deletions azuredevops/azuredevopsclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
Copyright 2023 The Flux CD contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azuredevops

import (
"context"
"fmt"
"net/url"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
)

// ProviderID is the provider ID for AzureDevops.
const ProviderID = gitprovider.ProviderID("azureDevops")

func newClient(c core.Client, g git.Client, domain string, destructiveActions bool) *ProviderClient {
ctx := &clientContext{
c,
g,
domain,
destructiveActions}
return &ProviderClient{
clientContext: ctx,
orgs: &OrganizationsClient{
clientContext: ctx,
},
repos: &RepositoriesClient{
clientContext: ctx,
},
}
}

// ProviderClient implements the gitprovider.Client interface.
var _ gitprovider.Client = &ProviderClient{}

type clientContext struct {
c core.Client
g git.Client
domain string
destructiveActions bool
}

// ProviderClient is the AzureDevops implementation of the gitprovider.Client interface.
type ProviderClient struct {
*clientContext

orgs *OrganizationsClient
repos *RepositoriesClient
}

// Raw returns the underlying AzureDevops client.
// It returns the core.Client
func (c *ProviderClient) Raw() interface{} {
return c.c
}

// UserRepositories returns the UserRepositoriesClient for the client.
func (c *ProviderClient) UserRepositories() gitprovider.UserRepositoriesClient {
// Method not support for AzureDevops
return nil
}

// OrgRepositories returns the OrgRepositoriesClient for the client.
func (c *ProviderClient) OrgRepositories() gitprovider.OrgRepositoriesClient {
return c.repos
}

// Organizations returns the OrganizationsClient for the client.
func (c *ProviderClient) Organizations() gitprovider.OrganizationsClient {
return c.orgs
}

// SupportedDomain returns the domain endpoint for this client, e.g. "dev.azure.com" or
// "my-custom-git-server.com:6443".
// This field is set at client creation time, and can't be changed.
func (c *ProviderClient) SupportedDomain() string {
u, _ := url.Parse(c.domain)
if u.Scheme == "" {
c.domain = fmt.Sprintf("https://%s", c.domain)
}
return c.domain
}

// ProviderID returns the provider ID for this client, e.g. "azuredevops".
func (c *ProviderClient) ProviderID() gitprovider.ProviderID {
return ProviderID
}

// HasTokenPermission returns whether the token has the given permission.
func (c *ProviderClient) HasTokenPermission(ctx context.Context, permission gitprovider.TokenPermission) (bool, error) {
return false, gitprovider.ErrNoProviderSupport
}
90 changes: 90 additions & 0 deletions azuredevops/client_organizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2023 The Flux CD contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azuredevops

import (
"context"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/core"
)

// OrganizationsClient implements the gitprovider.OrganizationsClient interface.
var _ gitprovider.OrganizationsClient = &OrganizationsClient{}

// OrganizationsClient operates on the projects a user has access to.
type OrganizationsClient struct {
*clientContext
}

// Get returns the specific project the user has access to.
func (c *OrganizationsClient) Get(ctx context.Context, ref gitprovider.OrganizationRef) (gitprovider.Organization, error) {
args := core.GetProjectArgs{ProjectId: &ref.Organization}
// https://pkg.go.dev/github.com/microsoft/azure-devops-go-api/azuredevops/[email protected]/core#ClientImpl.GetProject
project, err := c.c.GetProject(ctx, args)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, err
return nil, fmt.Errorf("failed getting project: %w", err)

}

ref.SetKey(project.Id.String())
return newProject(c.clientContext, *project, ref), nil
}

// List returns all available projects, using multiple requests if needed.
func (c *OrganizationsClient) List(ctx context.Context) ([]gitprovider.Organization, error) {
args := core.GetProjectsArgs{}
projects := make([]gitprovider.Organization, 0)

return c.list(ctx, args, projects)
}

func (c *OrganizationsClient) list(ctx context.Context, args core.GetProjectsArgs, projects []gitprovider.Organization) ([]gitprovider.Organization, error) {
// https://pkg.go.dev/github.com/microsoft/azure-devops-go-api/azuredevops/[email protected]/core#ClientImpl.GetProjects
apiObjs, err := c.c.GetProjects(ctx, args)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, err
return nil, fmt.Errorf("failed getting projects: %w", err)

}

for _, apiObj := range apiObjs.Value {
ref := gitprovider.OrganizationRef{
Domain: *apiObj.Url,
Organization: *apiObj.Name,
}

teamProject := core.TeamProject{
Id: apiObj.Id,
Name: apiObj.Name,
}

ref.SetKey(apiObj.Id.String())
projects = append(projects, newProject(c.clientContext, teamProject, ref))
}

if apiObjs.ContinuationToken != "" {
args := core.GetProjectsArgs{
ContinuationToken: &apiObjs.ContinuationToken,
}
return c.list(ctx, args, projects)
}

return projects, nil
}

// Children is not supported by AzureDevops.
func (c *OrganizationsClient) Children(_ context.Context, _ gitprovider.OrganizationRef) ([]gitprovider.Organization, error) {
return nil, gitprovider.ErrNoProviderSupport
}
Loading