-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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 DevOps SCM Provider Generator; add branchNormalized to SCM Generator template fields. #9283
Merged
crenshaw-dev
merged 11 commits into
argoproj:master
from
brinchm:feat/scmprovider-azuredevops
Jun 15, 2022
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
cdf0503
feat: add Azure DevOps SCM provider
brinchm 9fa5206
feat: add branchNormalized scm provider parameter.
brinchm e4826b2
add Azure DevOps SCM provider docs.
brinchm c9ae79e
add branchNormalized docs.
brinchm ccf23eb
Changes to Azure DevOps SCM generator from PR feeback.
brinchm 7eda041
fix linting error and failing tests.
brinchm 2e47bba
remove NoError assertion
brinchm a16529a
handle errors when repos are disabled, fix branch name in query when …
brinchm 1097eae
set proper example values for SCMProviderGeneratorAzureDevOps fields.
brinchm b6bd6c3
Merge remote origin branch 'master' into feat/scmprovider-azuredevops
brinchm 5a407bf
sort dependencies properly by name
brinchm 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package scm_provider | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
netUrl "net/url" | ||
"strings" | ||
|
||
"github.com/google/uuid" | ||
"github.com/microsoft/azure-devops-go-api/azuredevops" | ||
azureGit "github.com/microsoft/azure-devops-go-api/azuredevops/git" | ||
) | ||
|
||
const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com" | ||
|
||
type azureDevOpsErrorTypeKeyValuesType struct { | ||
GitRepositoryNotFound string | ||
GitItemNotFound string | ||
} | ||
|
||
var AzureDevOpsErrorsTypeKeyValues = azureDevOpsErrorTypeKeyValuesType{ | ||
GitRepositoryNotFound: "GitRepositoryNotFoundException", | ||
GitItemNotFound: "GitItemNotFoundException", | ||
} | ||
|
||
type AzureDevOpsClientFactory interface { | ||
// Returns an Azure Devops Client interface. | ||
GetClient(ctx context.Context) (azureGit.Client, error) | ||
} | ||
|
||
type devopsFactoryImpl struct { | ||
connection *azuredevops.Connection | ||
} | ||
|
||
func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (azureGit.Client, error) { | ||
gitClient, err := azureGit.NewClient(ctx, factory.connection) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get new Azure DevOps git client for SCM generator: %w", err) | ||
} | ||
return gitClient, nil | ||
} | ||
|
||
// Contains Azure Devops REST API implementation of SCMProviderService. | ||
// See https://docs.microsoft.com/en-us/rest/api/azure/devops | ||
|
||
type AzureDevOpsProvider struct { | ||
organization string | ||
teamProject string | ||
accessToken string | ||
clientFactory AzureDevOpsClientFactory | ||
allBranches bool | ||
} | ||
|
||
var _ SCMProviderService = &AzureDevOpsProvider{} | ||
var _ AzureDevOpsClientFactory = &devopsFactoryImpl{} | ||
|
||
func NewAzureDevOpsProvider(ctx context.Context, accessToken string, org string, url string, project string, allBranches bool) (*AzureDevOpsProvider, error) { | ||
if accessToken == "" { | ||
return nil, fmt.Errorf("no access token provided") | ||
} | ||
|
||
devOpsURL, err := getValidDevOpsURL(url, org) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
connection := azuredevops.NewPatConnection(devOpsURL, accessToken) | ||
|
||
return &AzureDevOpsProvider{organization: org, teamProject: project, accessToken: accessToken, clientFactory: &devopsFactoryImpl{connection: connection}, allBranches: allBranches}, nil | ||
} | ||
|
||
func (g *AzureDevOpsProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) { | ||
gitClient, err := g.clientFactory.GetClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err) | ||
} | ||
getRepoArgs := azureGit.GetRepositoriesArgs{Project: &g.teamProject} | ||
azureRepos, err := gitClient.GetRepositories(ctx, getRepoArgs) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
repos := []*Repository{} | ||
for _, azureRepo := range *azureRepos { | ||
if azureRepo.Name == nil || azureRepo.DefaultBranch == nil || azureRepo.RemoteUrl == nil || azureRepo.Id == nil { | ||
continue | ||
} | ||
repos = append(repos, &Repository{ | ||
Organization: g.organization, | ||
Repository: *azureRepo.Name, | ||
URL: *azureRepo.RemoteUrl, | ||
Branch: *azureRepo.DefaultBranch, | ||
Labels: []string{}, | ||
RepositoryId: *azureRepo.Id, | ||
}) | ||
} | ||
|
||
return repos, nil | ||
} | ||
|
||
func (g *AzureDevOpsProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) { | ||
gitClient, err := g.clientFactory.GetClient(ctx) | ||
if err != nil { | ||
return false, fmt.Errorf("failed to get Azure DevOps client: %w", err) | ||
} | ||
|
||
var repoId string | ||
if uuid, isUuid := repo.RepositoryId.(uuid.UUID); isUuid { //most likely an UUID, but do type-safe check anyway. Do %v fallback if not expected type. | ||
repoId = uuid.String() | ||
} else { | ||
repoId = fmt.Sprintf("%v", repo.RepositoryId) | ||
} | ||
|
||
branchName := repo.Branch | ||
getItemArgs := azureGit.GetItemArgs{RepositoryId: &repoId, Project: &g.teamProject, Path: &path, VersionDescriptor: &azureGit.GitVersionDescriptor{Version: &branchName}} | ||
_, err = gitClient.GetItem(ctx, getItemArgs) | ||
|
||
if err != nil { | ||
if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && wrappedError.TypeKey != nil { | ||
if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitItemNotFound { | ||
return false, nil | ||
} | ||
} | ||
|
||
return false, fmt.Errorf("failed to check for path existence in Azure DevOps: %w", err) | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
func (g *AzureDevOpsProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) { | ||
gitClient, err := g.clientFactory.GetClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err) | ||
} | ||
|
||
repos := []*Repository{} | ||
|
||
if !g.allBranches { | ||
defaultBranchName := strings.Replace(repo.Branch, "refs/heads/", "", 1) //Azure DevOps returns default branch info like 'refs/heads/main', but does not support branch lookup of this format. | ||
getBranchArgs := azureGit.GetBranchArgs{RepositoryId: &repo.Repository, Project: &g.teamProject, Name: &defaultBranchName} | ||
branchResult, err := gitClient.GetBranch(ctx, getBranchArgs) | ||
if err != nil { | ||
if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && wrappedError.TypeKey != nil { | ||
if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound { | ||
return repos, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("could not get default branch %v (%v) from repository %v: %w", defaultBranchName, repo.Branch, repo.Repository, err) | ||
} | ||
|
||
if branchResult.Name == nil || branchResult.Commit == nil { | ||
return nil, fmt.Errorf("invalid branch result after requesting branch %v from repository %v", repo.Branch, repo.Repository) | ||
} | ||
|
||
repos = append(repos, &Repository{ | ||
Branch: *branchResult.Name, | ||
SHA: *branchResult.Commit.CommitId, | ||
Organization: repo.Organization, | ||
Repository: repo.Repository, | ||
URL: repo.URL, | ||
Labels: []string{}, | ||
RepositoryId: repo.RepositoryId, | ||
}) | ||
|
||
return repos, nil | ||
} | ||
|
||
getBranchesRequest := azureGit.GetBranchesArgs{RepositoryId: &repo.Repository, Project: &g.teamProject} | ||
branches, err := gitClient.GetBranches(ctx, getBranchesRequest) | ||
if err != nil { | ||
if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && wrappedError.TypeKey != nil { | ||
if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound { | ||
return repos, nil | ||
} | ||
} | ||
return nil, fmt.Errorf("failed getting branches from repository %v, project %v: %w", repo.Repository, g.teamProject, err) | ||
} | ||
|
||
if branches == nil { | ||
return nil, fmt.Errorf("got empty branch result from repository %v, project %v: %w", repo.Repository, g.teamProject, err) | ||
} | ||
|
||
for _, azureBranch := range *branches { | ||
repos = append(repos, &Repository{ | ||
Branch: *azureBranch.Name, | ||
SHA: *azureBranch.Commit.CommitId, | ||
Organization: repo.Organization, | ||
Repository: repo.Repository, | ||
URL: repo.URL, | ||
Labels: []string{}, | ||
RepositoryId: repo.RepositoryId, | ||
}) | ||
} | ||
|
||
return repos, nil | ||
} | ||
|
||
func getValidDevOpsURL(url string, org string) (string, error) { | ||
if url == "" { | ||
url = AZURE_DEVOPS_DEFAULT_URL | ||
} | ||
separator := "" | ||
if !strings.HasSuffix(url, "/") { | ||
separator = "/" | ||
} | ||
|
||
devOpsURL := fmt.Sprintf("%s%s%s", url, separator, org) | ||
|
||
urlCheck, err := netUrl.ParseRequestURI(devOpsURL) | ||
|
||
if err != nil { | ||
return "", fmt.Errorf("got an invalid URL for the Azure SCM generator: %w", err) | ||
} | ||
|
||
ret := urlCheck.String() | ||
return ret, nil | ||
} |
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.
I wonder if this error should be bubbled up. Is this ignored because it applies to disabled repos? If it applies to more than than, should we log a warning?
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.
The
GitRepository
returned from the Azure DevOps Golang library doesn't include info indicating if a repo is disabled, but the REST API it calls, does.When retrieving branches for a disabled repo, the
WrappedError
contains theGitRepositoryNotFoundException
text. For this situation, I chose to ignore the error and just return an empty slice.I'll need to give this some thought; The
GetRepositories
call is quite simple, so a GET could be issued directly to the REST API... Might work, but it seems like a suboptimal solution.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.
Yeah, reinventing the wheel there would stink. I'm okay merging as-is. I don't think this will catch transient errors and accidentally delete Apps or anything nefarious like that.
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.
Thanks :)
The feature branch has one conflict in
go.mod
and some 129 commits behind master. I'll do a merge and push asap.