Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Azure/azure-dev into use-template-f…
Browse files Browse the repository at this point in the history
…or-pipeline-config-generation
  • Loading branch information
vhvb1989 committed Aug 3, 2024
2 parents 50a924c + 49a0b64 commit 391a724
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 56 deletions.
92 changes: 51 additions & 41 deletions cli/azd/pkg/pipeline/azdo_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"log"
"regexp"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/azdo"
Expand Down Expand Up @@ -418,56 +417,68 @@ func (p *AzdoScmProvider) promptForAzdoRepository(ctx context.Context, console i
return remoteUrl, nil
}

// defines the structure of an ssl git remote
var azdoRemoteGitUrlRegex = regexp.MustCompile(`^git@ssh.dev.azure\.com:(.*?)(?:\.git)?$`)

// defines the structure of an HTTPS git remote
var azdoRemoteHttpsUrlRegex = regexp.MustCompile(`^https://[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*:*.+@dev.azure\.com/(.*?)$`)

// ErrRemoteHostIsNotAzDo the error used when a non Azure DevOps remote is found
var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an Azure DevOps host")

// ErrSSHNotSupported the error used when ssh git remote is detected
var ErrSSHNotSupported = errors.New("ssh git remote is not supported. " +
"Use HTTPS git remote to connect the remote repository")

// helper function to determine if the provided remoteUrl is an azure devops repo.
// currently supports AzDo PaaS
func isAzDoRemote(remoteUrl string) error {
if azdoRemoteGitUrlRegex.MatchString(remoteUrl) {
return ErrSSHNotSupported
}
slug := ""
for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} {
captures := r.FindStringSubmatch(remoteUrl)
if captures != nil {
slug = captures[1]
type azdoRemote struct {
Project string
RepositoryName string
}

// parseAzDoRemote extracts the organization, project and repository name from an Azure DevOps remote url
// the url can be in the form of:
// - https://dev.azure.com/[org|user]/[project]/_git/[repo]
// - https://[user]@dev.azure.com/[org|user]/[project]/_git/[repo]
// - https://[org].visualstudio.com/[project]/_git/[repo]
// - git@ssh.dev.azure.com:v[1-3]/[user|org]/[project]/[repo]
// - git@vs-ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
// - git@ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
func parseAzDoRemote(remoteUrl string) (*azdoRemote, error) {
// Initialize the azdoRemote struct
azdoRemote := &azdoRemote{}

if !strings.Contains(remoteUrl, "visualstudio.com") && !strings.Contains(remoteUrl, "dev.azure.com") {
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}

if strings.Contains(remoteUrl, "/_git/") {
// applies to http or https
parts := strings.Split(remoteUrl, "/_git/")
projectNameStart := strings.LastIndex(parts[0], "/")
projectPartLen := len(parts[0])

if len(parts) != 2 || // remoteUrl must have exactly one "/_git/" substring
!strings.Contains(parts[0], "/") || // part 0 (the project) must have more than one "/"
projectPartLen <= 1 || // part 0 must be greater than 1 character
projectNameStart == projectPartLen-1 { // part 0 must not end with "/"
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}

azdoRemote.Project = parts[0][projectNameStart+1:]
azdoRemote.RepositoryName = parts[1]
return azdoRemote, nil
}
if slug == "" {
return ErrRemoteHostIsNotAzDo
}
return nil
}

func parseAzDoRemote(remoteUrl string) (string, error) {
for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} {
captures := r.FindStringSubmatch(remoteUrl)
if captures != nil {
return captures[1], nil
}
if strings.Contains(remoteUrl, "git@") {
// applies to git@ -> project and repo always in the last two parts
parts := strings.Split(remoteUrl, "/")
partsLen := len(parts)
azdoRemote.Project = parts[partsLen-2]
azdoRemote.RepositoryName = parts[partsLen-1]
return azdoRemote, nil
}
return "", nil

// If the remoteUrl does not match any of the supported formats, return an error
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}

// gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts
// like owner, name and path
func (p *AzdoScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) {
err := isAzDoRemote(remoteUrl)
if err != nil {
return nil, err
}

repoDetails := p.getRepoDetails()
// Try getting values from the env.
// This is a quick shortcut to avoid parsing the remote in detail.
Expand Down Expand Up @@ -496,17 +507,16 @@ func (p *AzdoScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string)
}

if repoDetails.projectId == "" || repoDetails.repoId == "" {
// Removing environment or creating a new one would remove any memory fro project
// Removing environment or creating a new one would remove any memory from project
// and repo. In that case, it needs to be calculated from the remote url
azdoSlug, err := parseAzDoRemote(remoteUrl)
azdoRemote, err := parseAzDoRemote(remoteUrl)
if err != nil {
return nil, fmt.Errorf("parsing Azure DevOps remote url: %s: %w", remoteUrl, err)
}
// azdoSlug => Org/Project/_git/repoName
parts := strings.Split(azdoSlug, "_git/")
repoDetails.projectName = strings.Split(parts[0], "/")[1]

repoDetails.projectName = azdoRemote.Project
p.env.DotenvSet(azdo.AzDoEnvironmentProjectName, repoDetails.projectName)
repoDetails.repoName = parts[1]
repoDetails.repoName = azdoRemote.RepositoryName
p.env.DotenvSet(azdo.AzDoEnvironmentRepoName, repoDetails.repoName)

connection, err := p.getAzdoConnection(ctx)
Expand Down
134 changes: 119 additions & 15 deletions cli/azd/pkg/pipeline/azdo_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,11 @@ func Test_azdo_provider_getRepoDetails(t *testing.T) {
require.EqualValues(t, false, details.pushStatus)
})

t.Run("ssh not supported", func(t *testing.T) {
// arrange
provider := getAzdoScmProviderTestHarness(mockinput.NewMockConsole())
ctx := context.Background()

// act
details, e := provider.gitRepoDetails(ctx, "git@ssh.dev.azure.com:v3/fake_org/repo1/repo1")

// assert
require.Error(t, e, ErrSSHNotSupported)
require.EqualValues(t, (*gitRepositoryDetails)(nil), details)
})

t.Run("non azure devops https remote", func(t *testing.T) {
//arrange
provider := &AzdoScmProvider{}
provider := &AzdoScmProvider{
env: environment.New("test"),
}
ctx := context.Background()

//act
Expand All @@ -66,7 +55,9 @@ func Test_azdo_provider_getRepoDetails(t *testing.T) {

t.Run("non azure devops git remote", func(t *testing.T) {
//arrange
provider := &AzdoScmProvider{}
provider := &AzdoScmProvider{
env: environment.New("test"),
}
ctx := context.Background()

//act
Expand Down Expand Up @@ -221,3 +212,116 @@ func getAzdoCiProviderTestHarness(console input.Console) *AzdoCiProvider {
console: console,
}
}

func Test_parseAzDoRemote(t *testing.T) {

// the url can be in the form of:
// - https://dev.azure.com/[org|user]/[project]/_git/[repo]
t.Run("valid HTTPS remote", func(t *testing.T) {
remoteUrl := "https://dev.azure.com/org/project/_git/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

// the url can be in the form of:
// - https://[user]@dev.azure.com/[org|user]/[project]/_git/[repo]
t.Run("valid user HTTPS remote", func(t *testing.T) {
remoteUrl := "https://user@visualstudio.com/org/project/_git/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

// the url can be in the form of:
// - https://[org].visualstudio.com/[project]/_git/[repo]
t.Run("valid legacy HTTPS remote", func(t *testing.T) {
remoteUrl := "https://visualstudio.com/org/project/_git/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

t.Run("valid legacy HTTPS remote with org", func(t *testing.T) {
remoteUrl := "https://org.visualstudio.com/org/project/_git/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

// the url can be in the form of:
// - git@ssh.dev.azure.com:v[1-3]/[user|org]/[project]/[repo]
// - git@vs-ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
// - git@ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
t.Run("valid SSH remote", func(t *testing.T) {
remoteUrl := "git@ssh.dev.azure.com:v3/org/project/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

t.Run("valid legacy SSH remote", func(t *testing.T) {
remoteUrl := "git@vs-ssh.visualstudio.com:v3/org/project/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

t.Run("valid legacy SSH remote", func(t *testing.T) {
remoteUrl := "git@ssh.visualstudio.com:v3/org/project/repo"
expected := &azdoRemote{
Project: "project",
RepositoryName: "repo",
}

result, err := parseAzDoRemote(remoteUrl)

require.NoError(t, err)
require.Equal(t, expected, result)
})

t.Run("invalid remote", func(t *testing.T) {
remoteUrl := "https://github.com/user/repo"

result, err := parseAzDoRemote(remoteUrl)

require.Error(t, err)
require.Nil(t, result)
})
}

0 comments on commit 391a724

Please sign in to comment.