mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
221 lines
7.0 KiB
Go
221 lines
7.0 KiB
Go
package scm_provider
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
netUrl "net/url"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
|
|
azureGit "github.com/microsoft/azure-devops-go-api/azuredevops/v7/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
|
|
clientFactory AzureDevOpsClientFactory
|
|
allBranches bool
|
|
}
|
|
|
|
var (
|
|
_ SCMProviderService = &AzureDevOpsProvider{}
|
|
_ AzureDevOpsClientFactory = &devopsFactoryImpl{}
|
|
)
|
|
|
|
func NewAzureDevOpsProvider(accessToken string, org string, url string, project string, allBranches bool) (*AzureDevOpsProvider, error) {
|
|
if accessToken == "" {
|
|
return nil, errors.New("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, clientFactory: &devopsFactoryImpl{connection: connection}, allBranches: allBranches}, nil
|
|
}
|
|
|
|
func (g *AzureDevOpsProvider) ListRepos(ctx context.Context, _ 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 {
|
|
var wrappedError azuredevops.WrappedError
|
|
if errors.As(err, &wrappedError) && 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 {
|
|
var wrappedError azuredevops.WrappedError
|
|
if errors.As(err, &wrappedError) && 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 {
|
|
var wrappedError azuredevops.WrappedError
|
|
if errors.As(err, &wrappedError) && 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
|
|
}
|