From 85fd327500a0a7e96f898d592be53c5190768dee Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 11 Jul 2024 01:21:03 +0000 Subject: [PATCH 1/3] support legacy hostname for azdo --- cli/azd/pkg/pipeline/azdo_provider.go | 86 ++++++++-------- cli/azd/pkg/pipeline/azdo_provider_test.go | 113 +++++++++++++++++++++ 2 files changed, 158 insertions(+), 41 deletions(-) diff --git a/cli/azd/pkg/pipeline/azdo_provider.go b/cli/azd/pkg/pipeline/azdo_provider.go index 228e2969363..8d4d4b14307 100644 --- a/cli/azd/pkg/pipeline/azdo_provider.go +++ b/cli/azd/pkg/pipeline/azdo_provider.go @@ -418,12 +418,6 @@ 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") @@ -431,43 +425,54 @@ var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an Azure DevOps 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] - } - } - if slug == "" { - return ErrRemoteHostIsNotAzDo - } - return nil +type azdoRemote struct { + Project string + RepositoryName string } -func parseAzDoRemote(remoteUrl string) (string, error) { - for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} { - captures := r.FindStringSubmatch(remoteUrl) - if captures != nil { - return captures[1], nil - } +var httpsRegex = regexp.MustCompile( + //nolint:lll + `^https:\/\/(?:.*)?(dev\.azure\.com|visualstudio\.com)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/_git\/([a-zA-Z0-9]+)$`) + +var sshRegex = regexp.MustCompile( + //nolint:lll + `^git@(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com|ssh\.visualstudio\.com):(v[1-3])?\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)$`) + +// 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{} + + // Check if the remoteUrl matches the HTTPS format + httpsMatches := httpsRegex.FindStringSubmatch(remoteUrl) + if len(httpsMatches) == 5 { + azdoRemote.Project = httpsMatches[3] + azdoRemote.RepositoryName = httpsMatches[4] + return azdoRemote, nil + } + + // Check if the remoteUrl matches the SSH format + sshMatches := sshRegex.FindStringSubmatch(remoteUrl) + if len(sshMatches) == 5 { + azdoRemote.Project = sshMatches[3] + azdoRemote.RepositoryName = sshMatches[4] + return azdoRemote, nil } - return "", nil + + // If the remoteUrl does not match any of the supported formats, return an error + return nil, fmt.Errorf("invalid Azure DevOps remote url") } // 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. @@ -496,17 +501,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) diff --git a/cli/azd/pkg/pipeline/azdo_provider_test.go b/cli/azd/pkg/pipeline/azdo_provider_test.go index 22247712ebd..3ae859e0395 100644 --- a/cli/azd/pkg/pipeline/azdo_provider_test.go +++ b/cli/azd/pkg/pipeline/azdo_provider_test.go @@ -221,3 +221,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) + }) +} From a4cd4d473635b54535c99f34097560a4bb5ebd55 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 11 Jul 2024 20:03:16 +0000 Subject: [PATCH 2/3] test updates --- cli/azd/pkg/pipeline/azdo_provider_test.go | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/pipeline/azdo_provider_test.go b/cli/azd/pkg/pipeline/azdo_provider_test.go index 3ae859e0395..deced61800d 100644 --- a/cli/azd/pkg/pipeline/azdo_provider_test.go +++ b/cli/azd/pkg/pipeline/azdo_provider_test.go @@ -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 @@ -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 From bf7b6ae2a4cedd224a4e504eaf51897def3210fa Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 23 Jul 2024 23:19:10 +0000 Subject: [PATCH 3/3] simplify with no regex --- cli/azd/pkg/pipeline/azdo_provider.go | 46 +++++++++++++++------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/cli/azd/pkg/pipeline/azdo_provider.go b/cli/azd/pkg/pipeline/azdo_provider.go index 8d4d4b14307..0d4c7a41cfd 100644 --- a/cli/azd/pkg/pipeline/azdo_provider.go +++ b/cli/azd/pkg/pipeline/azdo_provider.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "log" - "regexp" "strings" "github.com/azure/azure-dev/cli/azd/pkg/azdo" @@ -430,14 +429,6 @@ type azdoRemote struct { RepositoryName string } -var httpsRegex = regexp.MustCompile( - //nolint:lll - `^https:\/\/(?:.*)?(dev\.azure\.com|visualstudio\.com)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/_git\/([a-zA-Z0-9]+)$`) - -var sshRegex = regexp.MustCompile( - //nolint:lll - `^git@(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com|ssh\.visualstudio\.com):(v[1-3])?\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)$`) - // 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] @@ -450,24 +441,39 @@ func parseAzDoRemote(remoteUrl string) (*azdoRemote, error) { // Initialize the azdoRemote struct azdoRemote := &azdoRemote{} - // Check if the remoteUrl matches the HTTPS format - httpsMatches := httpsRegex.FindStringSubmatch(remoteUrl) - if len(httpsMatches) == 5 { - azdoRemote.Project = httpsMatches[3] - azdoRemote.RepositoryName = httpsMatches[4] + 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 } - // Check if the remoteUrl matches the SSH format - sshMatches := sshRegex.FindStringSubmatch(remoteUrl) - if len(sshMatches) == 5 { - azdoRemote.Project = sshMatches[3] - azdoRemote.RepositoryName = sshMatches[4] + 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 } // If the remoteUrl does not match any of the supported formats, return an error - return nil, fmt.Errorf("invalid Azure DevOps remote url") + return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl) } // gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts