From bdc335925fb43094c6762896a12233c0bc53a41c Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 13 Sep 2022 07:45:16 -0700 Subject: [PATCH 01/33] Adding changes to support debugging pipeline provider Co-authored-by: Hadwa Gaber --- .../pkg/commands/pipeline/pipeline_manager.go | 46 +++++++++++++------ cli/azd/test/functional/cli_test.go | 23 ++++++++++ 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 2fae8a16ec6..1ab0d34cc28 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -5,9 +5,11 @@ package pipeline import ( "context" + "encoding/json" "errors" "fmt" "log" + "os" "time" "github.com/azure/azure-dev/cli/azd/internal" @@ -200,14 +202,25 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { // changed from "az-cli" to "az-dev" manager.PipelineServicePrincipalName = fmt.Sprintf("az-dev-%s", time.Now().UTC().Format("01-02-2006-15-04-05")) } + inputConsole.Message(ctx, fmt.Sprintf("Creating or updating service principal %s.\n", manager.PipelineServicePrincipalName)) - credentials, err := azCli.CreateOrUpdateServicePrincipal( - ctx, - manager.Environment.GetSubscriptionId(), - manager.PipelineServicePrincipalName, - manager.PipelineRoleName) - if err != nil { - return fmt.Errorf("failed to create or update service principal: %w", err) + var credentials json.RawMessage = nil + var err error + + existingSP := os.Getenv("AZD_EXISTING_SP") + if existingSP != "" { + credentials = json.RawMessage(existingSP) + } + + if credentials == nil { + credentials, err = azCli.CreateOrUpdateServicePrincipal( + ctx, + manager.Environment.GetSubscriptionId(), + manager.PipelineServicePrincipalName, + manager.PipelineRoleName) + if err != nil { + return fmt.Errorf("failed to create or update service principal: %w", err) + } } // Get git repo details @@ -239,14 +252,17 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { return err } - // The CI pipeline should be set-up and ready at this point. - // azd offers to push changes to the scm to start a new pipeline run - doPush, err := inputConsole.Confirm(ctx, input.ConsoleOptions{ - Message: "Would you like to commit and push your local changes to start the configured CI pipeline?", - DefaultValue: true, - }) - if err != nil { - return fmt.Errorf("prompting to push: %w", err) + var doPush bool = true + if existingSP == "" { + // The CI pipeline should be set-up and ready at this point. + // azd offers to push changes to the scm to start a new pipeline run + doPush, err = inputConsole.Confirm(ctx, input.ConsoleOptions{ + Message: "Would you like to commit and push your local changes to start the configured CI pipeline?", + DefaultValue: true, + }) + if err != nil { + return fmt.Errorf("prompting to push: %w", err) + } } currentBranch, err := git.NewGitCli(ctx).GetCurrentBranch(ctx, manager.AzdCtx.ProjectDirectory()) diff --git a/cli/azd/test/functional/cli_test.go b/cli/azd/test/functional/cli_test.go index 9abb784c81c..bed8bd5a372 100644 --- a/cli/azd/test/functional/cli_test.go +++ b/cli/azd/test/functional/cli_test.go @@ -815,6 +815,29 @@ func Test_CLI_InfraCreateAndDeleteResourceTerraformRemote(t *testing.T) { t.Logf("Done\n") } +func Test_CLI_PipelineConfig(t *testing.T) { + // running this test in parallel is ok as it uses a t.TempDir() + t.Parallel() + // ctx, cancel := newTestContext(t) + // defer cancel() + + dir := "/Users/hattanshobokshi/projects/azd/cse-fork/tests/azdo7" //t.TempDir() + t.Logf("DIR: %s", dir) + + envName := "azdtest7" //randomEnvName() + t.Logf("AZURE_ENV_NAME: %s", envName) + + cli := azdcli.NewCLI(t) + cli.WorkingDirectory = dir + cli.Env = append(os.Environ(), "AZURE_LOCATION=eastus2") + + t.Logf("Starting pipeline config\n") + err := cmd.Execute([]string{"pipeline", "config", "--cwd", dir, "--principal-name", "mock-principal"}) + require.NoError(t, err) + + t.Logf("Done\n") +} + func TestMain(m *testing.M) { flag.Parse() shortFlag := flag.Lookup("test.short") From 32bc049a2156359693c00ad513c1fe09e6ba4cbc Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 13 Sep 2022 07:47:31 -0700 Subject: [PATCH 02/33] Removing local debug values Co-authored-by: Hadwa Gaber --- cli/azd/test/functional/cli_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/azd/test/functional/cli_test.go b/cli/azd/test/functional/cli_test.go index bed8bd5a372..99b84f9f8c4 100644 --- a/cli/azd/test/functional/cli_test.go +++ b/cli/azd/test/functional/cli_test.go @@ -818,13 +818,11 @@ func Test_CLI_InfraCreateAndDeleteResourceTerraformRemote(t *testing.T) { func Test_CLI_PipelineConfig(t *testing.T) { // running this test in parallel is ok as it uses a t.TempDir() t.Parallel() - // ctx, cancel := newTestContext(t) - // defer cancel() - dir := "/Users/hattanshobokshi/projects/azd/cse-fork/tests/azdo7" //t.TempDir() + dir := t.TempDir() t.Logf("DIR: %s", dir) - envName := "azdtest7" //randomEnvName() + envName := randomEnvName() t.Logf("AZURE_ENV_NAME: %s", envName) cli := azdcli.NewCLI(t) From a4467f55b6e8cc1c6755b6ee8546fd24f3fb596a Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 13 Sep 2022 16:41:06 -0700 Subject: [PATCH 03/33] add support for project selection Co-authored-by: Hadwa Gaber --- .vscode/cspell.global.yaml | 1 + cli/azd/cmd/pipeline.go | 8 +- cli/azd/pkg/commands/pipeline/azdo.go | 82 +++++++++++++++++++ .../pkg/commands/pipeline/azdo_provider.go | 74 ++++++++++++++++- .../pkg/commands/pipeline/github_provider.go | 1 + go.mod | 1 + go.sum | 12 +-- 7 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 cli/azd/pkg/commands/pipeline/azdo.go diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 6b082ae791e..ed42ab95cc7 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -95,6 +95,7 @@ ignoreWords: - aztfmod - menuid - PLACEHOLDERIACTOOLS + - Azdo useGitignore: true dictionaryDefinitions: - name: gitHubUserAliases diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index f66d07e065d..bd3060bf576 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -98,8 +98,12 @@ func (p *pipelineConfigAction) Run( // Using GitHub by default for now. To be updated to either GitHub or Azdo. // The CI provider might need to have a reference to the SCM provider if its implementation // will depend on where is the SCM. For example, azdo support any SCM source. - p.manager.ScmProvider = &pipeline.GitHubScmProvider{} - p.manager.CiProvider = &pipeline.GitHubCiProvider{} + p.manager.ScmProvider = &pipeline.AzdoHubScmProvider{ + Env: &env, + } + p.manager.CiProvider = &pipeline.AzdoCiProvider{ + Env: &env, + } // set context for manager p.manager.AzdCtx = azdCtx diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go new file mode 100644 index 00000000000..46e7568e39c --- /dev/null +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -0,0 +1,82 @@ +package pipeline + +import ( + "context" + "fmt" + "os" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" + + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/core" +) + +var ( + // The hostname of the AzDo PaaS service. + AzDoHostName = "dev.azure.com" + AzDoPatName = "AZURE_DEVOPS_EXT_PAT" + AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" +) + +type AzDoClient struct { + core.Client +} + +func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, key string, label string) (string, error) { + value := env.Values[key] + if value != "" { + return value, nil + } + + value, exists := os.LookupEnv(key) + if !exists || value == "" { + return value, fmt.Errorf("%s not found in environment variable %s", label, key) + } + return value, nil +} + +func ensureAzdoPatExists(ctx context.Context, env *environment.Environment) (string, error) { + return ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") +} + +func ensureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { + return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") + +} + +func getAzdoConnection(ctx context.Context, organization string, personalAccessToken string) *azuredevops.Connection { + organizationUrl := fmt.Sprintf("https://dev.azure.com/%s", organization) + connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken) + return connection +} + +func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return "", err + } + + args := core.GetProjectsArgs{} + getProjectsResponse, err := coreClient.GetProjects(ctx, args) + if err != nil { + return "", err + } + + projects := getProjectsResponse.Value + options := make([]string, len(projects)) + for idx, project := range projects { + options[idx] = *project.Name + } + + projectIdx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Please choose an existing Azure DevOps Project", + Options: options, + }) + + if err != nil { + return "", fmt.Errorf("prompting for azdo project: %w", err) + } + + return options[projectIdx], nil +} diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 073923e3144..8d0a57a3c37 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" @@ -17,6 +18,8 @@ import ( // AzdoHubScmProvider implements ScmProvider using Azure DevOps as the provider // for source control manager. type AzdoHubScmProvider struct { + projectName string + Env *environment.Environment } // *** subareaProvider implementation ****** @@ -30,7 +33,13 @@ func (p *AzdoHubScmProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck check the current state of external tools and any // other dependency to be as expected for execution. func (p *AzdoHubScmProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - return errors.New("not implemented") + _, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return err + } + + _, err = ensureAzdoOrgNameExists(ctx, p.Env) + return err } // name returns the name of the provider @@ -39,15 +48,69 @@ func (p *AzdoHubScmProvider) name() string { } // *** scmProvider implementation ****** +func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, error) { + org, err := ensureAzdoOrgNameExists(ctx, p.Env) + if err != nil { + return "", err + } + + pat, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return "", err + } + + connection := getAzdoConnection(ctx, org, pat) + + idx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "How would you like to configure your project?", + Options: []string{ + "Select an existing Azure DevOps project", + "Create a new Azure DevOps Project", + }, + DefaultValue: "Create a new Azure DevOps Project", + }) + if err != nil { + return "", fmt.Errorf("prompting for azdo project type: %w", err) + } + + var projectName string + switch idx { + // Select from an existing AzDo project + case 0: + name, err := getAzdoProjectFromExisting(ctx, connection, console) + if err != nil { + return "", err + } + projectName = name + // Create a new AzDo project + case 1: + fmt.Println("New Azdo") + default: + panic(fmt.Sprintf("unexpected selection index %d", idx)) + } + _ = idx + return projectName, nil +} + +func (p *AzdoHubScmProvider) supportsProjects() bool { + return true +} // configureGitRemote set up or create the git project and git remote func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath string, remoteName string, console input.Console) (string, error) { + return "remoteUrl", errors.New("not implemented") } // gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts // like owner, name and path func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) { + inputConsole := input.GetConsole(ctx) + projectName, err := p.ensureProjectExists(ctx, inputConsole) + if err != nil { + return nil, err + } + p.projectName = projectName return &gitRepositoryDetails{ owner: "", repoName: "", @@ -66,6 +129,7 @@ func (p *AzdoHubScmProvider) preventGitPush( // AzdoCiProvider implements a CiProvider using Azure DevOps to manage CI with azdo pipelines. type AzdoCiProvider struct { + Env *environment.Environment } // *** subareaProvider implementation ****** @@ -77,7 +141,13 @@ func (p *AzdoCiProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck nil for Azdo func (p *AzdoCiProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - return errors.New("not implemented") + _, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return err + } + + _, err = ensureAzdoOrgNameExists(ctx, p.Env) + return err } // name returns the name of the provider. diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index 03320f113fd..77d001c3c7c 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -27,6 +27,7 @@ import ( // for source control manager. type GitHubScmProvider struct { newGitHubRepoCreated bool + environment *environment.Environment } // *** subareaProvider implementation ****** diff --git a/go.mod b/go.mod index 7af60a71c86..1a0a6429e25 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-isatty v0.0.14 github.com/microsoft/ApplicationInsights-Go v0.4.4 + github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 github.com/otiai10/copy v1.7.0 github.com/pbnj/go-open v0.1.1 github.com/sethvargo/go-retry v0.2.3 diff --git a/go.sum b/go.sum index fac5f902652..8629cc14adc 100644 --- a/go.sum +++ b/go.sum @@ -188,7 +188,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -209,6 +208,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -304,6 +304,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -397,7 +399,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= @@ -433,7 +434,6 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -472,8 +472,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -516,7 +514,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -597,7 +594,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -610,7 +606,6 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -684,7 +679,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 4d9501fd3ea619cadafc5aabad7a63fc79208763 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 13 Sep 2022 17:46:22 -0700 Subject: [PATCH 04/33] Adding Azdo remote validate. --- .../pkg/commands/pipeline/azdo_provider.go | 79 ++++++++++++++++++- cli/azd/pkg/commands/pipeline/pipeline.go | 2 + .../pkg/commands/pipeline/pipeline_manager.go | 2 +- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 8d0a57a3c37..0e30b436736 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "regexp" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" @@ -18,9 +19,12 @@ import ( // AzdoHubScmProvider implements ScmProvider using Azure DevOps as the provider // for source control manager. type AzdoHubScmProvider struct { - projectName string + repoDetails *AzdoRepositoryDetails Env *environment.Environment } +type AzdoRepositoryDetails struct { + projectName string +} // *** subareaProvider implementation ****** @@ -98,23 +102,92 @@ func (p *AzdoHubScmProvider) supportsProjects() bool { // configureGitRemote set up or create the git project and git remote func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath string, remoteName string, console input.Console) (string, error) { + projectName, err := p.ensureProjectExists(ctx, console) + if err != nil { + return "", err + } + + repoDetails := &AzdoRepositoryDetails{ + projectName: projectName, + } + p.repoDetails = repoDetails + + // There are a few ways to configure the remote so offer a choice to the user. + idx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "How would you like to configure your remote?", + Options: []string{ + fmt.Sprintf("Select an existing Azure Devops Repository (Organization: %s)", p.repoDetails.projectName), + fmt.Sprintf("Create a new private Azure Devops repository (Organization: %s)", p.repoDetails.projectName), + }, + DefaultValue: "Create a new private GitHub repository", + }) + + if err != nil { + return "", fmt.Errorf("prompting for remote configuration type: %w", err) + } + + switch idx { + // Select from an existing GitHub project + case 0: + fmt.Println("Existing") + // Create a new project + case 1: + fmt.Println("New") + // Enter a URL directly. + case 2: + fmt.Println("Prompt") + default: + panic(fmt.Sprintf("unexpected selection index %d", idx)) + } return "remoteUrl", errors.New("not implemented") } +// 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") + +func isAzDoRemote(remoteUrl string) error { + slug := "" + for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} { + captures := r.FindStringSubmatch(remoteUrl) + if captures != nil { + slug = captures[1] + } + } + if slug == "" { + return ErrRemoteHostIsNotAzDo + } + return nil +} + // gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts // like owner, name and path func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) { + err := isAzDoRemote(remoteUrl) + if err != nil { + return nil, err + } + inputConsole := input.GetConsole(ctx) projectName, err := p.ensureProjectExists(ctx, inputConsole) if err != nil { return nil, err } - p.projectName = projectName + repoDetails := &AzdoRepositoryDetails{ + projectName: projectName, + } + p.repoDetails = repoDetails return &gitRepositoryDetails{ owner: "", repoName: "", - }, errors.New("not implemented") + details: repoDetails, + }, nil } // preventGitPush is nil for Azure DevOps diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index b130990207a..8d680e44920 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -34,6 +34,8 @@ type gitRepositoryDetails struct { repoName string // System path where the git project is gitProjectPath string + + details interface{} } // ScmProvider defines the base behavior for a source control manager provider. diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 1ab0d34cc28..504877f5b81 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -226,7 +226,7 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { // Get git repo details gitRepoInfo, err := manager.getGitRepoDetails(ctx) if err != nil { - return fmt.Errorf("ensuring github remote: %w", err) + return fmt.Errorf("ensuring git remote: %w", err) } // config pipeline handles setting or creating the provider pipeline to be used From 351502a59e2efd3288cce1c5a10bd9588b766076 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Wed, 14 Sep 2022 09:00:45 -0700 Subject: [PATCH 05/33] Configure git remote for azdo and select Repo --- cli/azd/cmd/pipeline.go | 1 - cli/azd/pkg/commands/pipeline/azdo.go | 48 +++++++ .../pkg/commands/pipeline/azdo_provider.go | 119 ++++++++++++++---- 3 files changed, 141 insertions(+), 27 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index bd3060bf576..16929b58a59 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -104,7 +104,6 @@ func (p *pipelineConfigAction) Run( p.manager.CiProvider = &pipeline.AzdoCiProvider{ Env: &env, } - // set context for manager p.manager.AzdCtx = azdCtx p.manager.Environment = env diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 46e7568e39c..c41eb35db26 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/core" + "github.com/microsoft/azure-devops-go-api/azuredevops/git" ) var ( @@ -51,6 +52,53 @@ func getAzdoConnection(ctx context.Context, organization string, personalAccessT return connection } +func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + includeLinks := true + includeAllUrls := true + repoArgs := git.GetRepositoriesArgs{ + Project: &projectName, + IncludeLinks: &includeLinks, + IncludeAllUrls: &includeAllUrls, + } + + getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) + if err != nil { + return nil, err + } + repos := *getRepositoriesResult + + // If there is only one repo in the project, skip asking the user to select a repo + if len(repos) == 1 { + return &repos[0], nil + } + + options := make([]string, len(repos)) + for idx, repo := range repos { + options[idx] = *repo.Name + } + repoIdx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Please choose an existing Azure DevOps Repository", + Options: options, + }) + + if err != nil { + return nil, fmt.Errorf("prompting for azdo project: %w", err) + } + selectedRepoName := options[repoIdx] + for _, repo := range repos { + if selectedRepoName == *repo.Name { + return &repo, nil + } + } + + return nil, fmt.Errorf("Error finding git repository %s in organization %s", selectedRepoName, orgName) +} + func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 0e30b436736..2fd10ba3753 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -9,21 +9,28 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/microsoft/azure-devops-go-api/azuredevops" ) // AzdoHubScmProvider implements ScmProvider using Azure DevOps as the provider // for source control manager. type AzdoHubScmProvider struct { - repoDetails *AzdoRepositoryDetails - Env *environment.Environment + repoDetails *AzdoRepositoryDetails + Env *environment.Environment + azdoConnection *azuredevops.Connection } type AzdoRepositoryDetails struct { projectName string + orgName string + repoName string + remoteUrl string + sshUrl string } // *** subareaProvider implementation ****** @@ -52,19 +59,75 @@ func (p *AzdoHubScmProvider) name() string { } // *** scmProvider implementation ****** -func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, error) { - org, err := ensureAzdoOrgNameExists(ctx, p.Env) +func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, console input.Console) (string, error) { + if p.repoDetails != nil && p.repoDetails.repoName != "" { + return p.repoDetails.remoteUrl, nil + } + + connection, err := p.getAzdoConnection(ctx) if err != nil { return "", err } + repo, err := getAzDoGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console) + if err != nil { + return "", err + } + + repoDetails := p.getRepoDetails() + repoDetails.repoName = *repo.Name + repoDetails.remoteUrl = *repo.RemoteUrl + repoDetails.sshUrl = *repo.SshUrl + + remoteParts := strings.Split(repoDetails.remoteUrl, "@") + if len(remoteParts) < 2 { + return "", fmt.Errorf("invalid azure devops remote") + } + remoteUser := remoteParts[0] + remoteHost := remoteParts[1] pat, err := ensureAzdoPatExists(ctx, p.Env) if err != nil { return "", err } + updatedRemote := fmt.Sprintf("%s:%s@%s", remoteUser, pat, remoteHost) - connection := getAzdoConnection(ctx, org, pat) + return updatedRemote, nil +} +func (p *AzdoHubScmProvider) getRepoDetails() *AzdoRepositoryDetails { + if p.repoDetails != nil { + return p.repoDetails + } + repoDetails := &AzdoRepositoryDetails{} + p.repoDetails = repoDetails + return p.repoDetails +} + +func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevops.Connection, error) { + if p.azdoConnection != nil { + return p.azdoConnection, nil + } + + org, err := ensureAzdoOrgNameExists(ctx, p.Env) + if err != nil { + return nil, err + } + + repoDetails := p.getRepoDetails() + repoDetails.orgName = org + + pat, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return nil, err + } + + connection := getAzdoConnection(ctx, org, pat) + return connection, nil +} +func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, error) { + if p.repoDetails != nil && p.repoDetails.projectName != "" { + return p.repoDetails.projectName, nil + } idx, err := console.Select(ctx, input.ConsoleOptions{ Message: "How would you like to configure your project?", Options: []string{ @@ -77,6 +140,11 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in return "", fmt.Errorf("prompting for azdo project type: %w", err) } + connection, err := p.getAzdoConnection(ctx) + if err != nil { + return "", err + } + var projectName string switch idx { // Select from an existing AzDo project @@ -96,10 +164,6 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in return projectName, nil } -func (p *AzdoHubScmProvider) supportsProjects() bool { - return true -} - // configureGitRemote set up or create the git project and git remote func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath string, remoteName string, console input.Console) (string, error) { projectName, err := p.ensureProjectExists(ctx, console) @@ -107,29 +171,33 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st return "", err } - repoDetails := &AzdoRepositoryDetails{ - projectName: projectName, - } - p.repoDetails = repoDetails + repoDetails := p.getRepoDetails() + repoDetails.projectName = projectName // There are a few ways to configure the remote so offer a choice to the user. idx, err := console.Select(ctx, input.ConsoleOptions{ - Message: "How would you like to configure your remote?", + Message: fmt.Sprintf("How would you like to configure your remote? (Organization: %s)", p.repoDetails.projectName), Options: []string{ - fmt.Sprintf("Select an existing Azure Devops Repository (Organization: %s)", p.repoDetails.projectName), - fmt.Sprintf("Create a new private Azure Devops repository (Organization: %s)", p.repoDetails.projectName), + "Select an existing Azure Devops Repository", + "Create a new private Azure Devops Repository", }, - DefaultValue: "Create a new private GitHub repository", + DefaultValue: "Create a new private Azure Devops Repository", }) if err != nil { return "", fmt.Errorf("prompting for remote configuration type: %w", err) } + var remoteUrl string + switch idx { // Select from an existing GitHub project case 0: - fmt.Println("Existing") + remoteUrl, err = p.ensureGitRepositoryExists(ctx, console) + if err != nil { + return "", err + } + // Create a new project case 1: fmt.Println("New") @@ -140,14 +208,14 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st panic(fmt.Sprintf("unexpected selection index %d", idx)) } - return "remoteUrl", errors.New("not implemented") + 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/(.*?)$`) +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") @@ -179,13 +247,12 @@ func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl strin if err != nil { return nil, err } - repoDetails := &AzdoRepositoryDetails{ - projectName: projectName, - } - p.repoDetails = repoDetails + repoDetails := p.getRepoDetails() + repoDetails.projectName = projectName + return &gitRepositoryDetails{ - owner: "", - repoName: "", + owner: p.repoDetails.orgName, + repoName: p.repoDetails.repoName, details: repoDetails, }, nil } From 0bbd031e0bb3c551ee767f7dbd40355e714eea26 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Wed, 14 Sep 2022 16:53:19 -0700 Subject: [PATCH 06/33] Added changes to support git push with PAT and credential helper --- cli/azd/cmd/pipeline.go | 7 +- .../pkg/commands/pipeline/azdo_provider.go | 66 +++++++++++++++++-- .../pkg/commands/pipeline/github_provider.go | 9 +++ cli/azd/pkg/commands/pipeline/pipeline.go | 6 ++ .../pkg/commands/pipeline/pipeline_manager.go | 6 ++ cli/azd/pkg/tools/git/git.go | 36 ++++++++++ 6 files changed, 123 insertions(+), 7 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 16929b58a59..4bac64b639f 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -99,11 +99,14 @@ func (p *pipelineConfigAction) Run( // The CI provider might need to have a reference to the SCM provider if its implementation // will depend on where is the SCM. For example, azdo support any SCM source. p.manager.ScmProvider = &pipeline.AzdoHubScmProvider{ - Env: &env, + Env: &env, + AzdContext: azdCtx, } p.manager.CiProvider = &pipeline.AzdoCiProvider{ - Env: &env, + Env: &env, + AzdContext: azdCtx, } + // set context for manager p.manager.AzdCtx = azdCtx p.manager.Environment = env diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 2fd10ba3753..42b517d9d6f 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -12,9 +12,11 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/azure/azure-dev/cli/azd/pkg/tools/git" "github.com/microsoft/azure-devops-go-api/azuredevops" ) @@ -23,6 +25,7 @@ import ( type AzdoHubScmProvider struct { repoDetails *AzdoRepositoryDetails Env *environment.Environment + AzdContext *azdcontext.AzdContext azdoConnection *azuredevops.Connection } type AzdoRepositoryDetails struct { @@ -160,7 +163,6 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in default: panic(fmt.Sprintf("unexpected selection index %d", idx)) } - _ = idx return projectName, nil } @@ -264,12 +266,65 @@ func (p *AzdoHubScmProvider) preventGitPush( remoteName string, branchName string, console input.Console) (bool, error) { - return false, errors.New("not implemented") + + gitCli := git.NewGitCli(ctx) + foundHelper, err := gitCli.CheckConfigCredentialHelper(ctx) + if err != nil { + return false, err + } + if !foundHelper { + fmt.Println(` + A credential helper is not configured for git. + This will require you to enter your Azure DevOps PAT when executing a git push. + https://git-scm.com/docs/git-credential-store + `) + idx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Would you like to enable credential.helper store for this local git repository?", + Options: []string{ + "Yes - set credential.helper = store", + "No - do not configure credential.helper", + }, + DefaultValue: "Yes - set credential.helper = store", + }) + if err != nil { + return false, fmt.Errorf("prompting for credential helper: %w", err) + } + switch idx { + // Configure Credential Store + case 0: + gitCli.SetCredentialStore(ctx, p.AzdContext.ProjectDirectory()) + // Skip + case 1: + break + default: + panic(fmt.Sprintf("unexpected selection index %d", idx)) + } + } + + return false, nil +} + +// preventGitPush is nil for Azure DevOps +func (p *AzdoHubScmProvider) postGitPush( + ctx context.Context, + gitRepo *gitRepositoryDetails, + remoteName string, + branchName string, + console input.Console) (bool, error) { + + gitCli := git.NewGitCli(ctx) + + //Reset remote to original url without PAT + gitCli.UpdateRemote(ctx, p.AzdContext.ProjectDirectory(), remoteName, p.repoDetails.remoteUrl) + + return false, nil } // AzdoCiProvider implements a CiProvider using Azure DevOps to manage CI with azdo pipelines. type AzdoCiProvider struct { - Env *environment.Environment + Env *environment.Environment + AzdContext *azdcontext.AzdContext + ScmProvider *ScmProvider } // *** subareaProvider implementation ****** @@ -306,10 +361,11 @@ func (p *AzdoCiProvider) configureConnection( credentials json.RawMessage, console input.Console) error { - return errors.New("not implemented") + return nil } // configurePipeline create Azdo pipeline func (p *AzdoCiProvider) configurePipeline(ctx context.Context) error { - return errors.New("not implemented") + + return nil } diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index 77d001c3c7c..1f144f5e7f7 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -150,6 +150,15 @@ func (p *GitHubScmProvider) preventGitPush( return false, nil } +func (p *GitHubScmProvider) postGitPush( + ctx context.Context, + gitRepo *gitRepositoryDetails, + remoteName string, + branchName string, + console input.Console) (bool, error) { + return false, nil +} + // enum type for taking a choice after finding GitHub actions disabled. type gitHubActionsEnablingChoice int diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 8d680e44920..c814e1c2512 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -56,6 +56,12 @@ type ScmProvider interface { remoteName string, branchName string, console input.Console) (bool, error) + //Hook function to allow SCM providers to handle scenarios after the git push is complete + postGitPush(ctx context.Context, + gitRepo *gitRepositoryDetails, + remoteName string, + branchName string, + console input.Console) (bool, error) } // CiProvider defines the base behavior for a continuous integration provider. diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 504877f5b81..52bda7a3620 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -292,6 +292,12 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { if err != nil { return err } + manager.ScmProvider.postGitPush( + ctx, + gitRepoInfo, + manager.PipelineRemoteName, + currentBranch, + inputConsole) } else { inputConsole.Message(ctx, fmt.Sprintf( diff --git a/cli/azd/pkg/tools/git/git.go b/cli/azd/pkg/tools/git/git.go index 15f8f835c5e..d549056c43b 100644 --- a/cli/azd/pkg/tools/git/git.go +++ b/cli/azd/pkg/tools/git/git.go @@ -23,11 +23,14 @@ type GitCli interface { FetchCode(ctx context.Context, repositoryPath string, branch string, target string) error InitRepo(ctx context.Context, repositoryPath string) error AddRemote(ctx context.Context, repositoryPath string, remoteName string, remoteUrl string) error + UpdateRemote(ctx context.Context, repositoryPath string, remoteName string, remoteUrl string) error GetCurrentBranch(ctx context.Context, repositoryPath string) (string, error) AddFile(ctx context.Context, repositoryPath string, filespec string) error Commit(ctx context.Context, repositoryPath string, message string) error PushUpstream(ctx context.Context, repositoryPath string, origin string, branch string) error IsUntrackedFile(ctx context.Context, repositoryPath string, filePath string) (bool, error) + SetCredentialStore(ctx context.Context, repositoryPath string) error + CheckConfigCredentialHelper(ctx context.Context) (bool, error) } type gitCli struct { @@ -143,6 +146,29 @@ func (cli *gitCli) InitRepo(ctx context.Context, repositoryPath string) error { return nil } +func (cli *gitCli) CheckConfigCredentialHelper(ctx context.Context) (bool, error) { + found, err := tools.ToolInPath("git") + if !found { + return false, err + } + gitRes, err := tools.ExecuteCommand(ctx, "git", "config", "credential.helper") + if err != nil { + return false, fmt.Errorf("checking %s credential.helper: %w", cli.Name(), err) + } + + return gitRes != "", nil +} + +func (cli *gitCli) SetCredentialStore(ctx context.Context, repositoryPath string) error { + runArgs := exec.NewRunArgs("git", "-C", repositoryPath, "config", "credential.helper", "store") + res, err := cli.commandRunner.Run(ctx, runArgs) + if err != nil { + return fmt.Errorf("failed to set credential store repository: %s: %w", res.String(), err) + } + + return nil +} + func (cli *gitCli) AddRemote(ctx context.Context, repositoryPath string, remoteName string, remoteUrl string) error { runArgs := exec.NewRunArgs("git", "-C", repositoryPath, "remote", "add", remoteName, remoteUrl) res, err := cli.commandRunner.Run(ctx, runArgs) @@ -153,6 +179,16 @@ func (cli *gitCli) AddRemote(ctx context.Context, repositoryPath string, remoteN return nil } +func (cli *gitCli) UpdateRemote(ctx context.Context, repositoryPath string, remoteName string, remoteUrl string) error { + runArgs := exec.NewRunArgs("git", "-C", repositoryPath, "remote", "set-url", remoteName, remoteUrl) + res, err := cli.commandRunner.Run(ctx, runArgs) + if err != nil { + return fmt.Errorf("failed to add remote: %s: %w", res.String(), err) + } + + return nil +} + func (cli *gitCli) AddFile(ctx context.Context, repositoryPath string, filespec string) error { runArgs := exec.NewRunArgs("git", "-C", repositoryPath, "add", filespec) res, err := cli.commandRunner.Run(ctx, runArgs) From e7d6ec9b109efb205ab4930b0684255ba5402af5 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Thu, 15 Sep 2022 01:36:32 -0700 Subject: [PATCH 07/33] adding support for create project & azdo details in .env --- cli/azd/pkg/commands/pipeline/azdo.go | 165 ++++++++++++++++-- .../pkg/commands/pipeline/azdo_provider.go | 83 +++++++-- .../pkg/commands/pipeline/pipeline_manager.go | 6 +- 3 files changed, 220 insertions(+), 34 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index c41eb35db26..5ee172d42c3 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -3,7 +3,11 @@ package pipeline import ( "context" "fmt" + "log" "os" + "path/filepath" + "strings" + "time" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/input" @@ -11,13 +15,18 @@ import ( "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/core" "github.com/microsoft/azure-devops-go-api/azuredevops/git" + "github.com/microsoft/azure-devops-go-api/azuredevops/operations" ) var ( // The hostname of the AzDo PaaS service. - AzDoHostName = "dev.azure.com" - AzDoPatName = "AZURE_DEVOPS_EXT_PAT" - AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" + AzDoHostName = "dev.azure.com" + AzDoPatName = "AZURE_DEVOPS_EXT_PAT" + AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" + AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" + AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" + AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" + AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" ) type AzDoClient struct { @@ -72,11 +81,6 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or } repos := *getRepositoriesResult - // If there is only one repo in the project, skip asking the user to select a repo - if len(repos) == 1 { - return &repos[0], nil - } - options := make([]string, len(repos)) for idx, repo := range repos { options[idx] = *repo.Name @@ -99,22 +103,157 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or return nil, fmt.Errorf("Error finding git repository %s in organization %s", selectedRepoName, orgName) } -func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, error) { - coreClient, err := core.NewClient(ctx, connection) +func GetProcessTemplateId(ctx context.Context, client core.Client) (string, error) { + processArgs := core.GetProcessesArgs{} + processes, err := client.GetProcesses(ctx, processArgs) + _ = processes if err != nil { return "", err } + process := (*processes)[0] + return fmt.Sprintf("%s", process.Id), nil +} + +func createAzdoProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + processTemplateId, err := GetProcessTemplateId(ctx, coreClient) + if err != nil { + return nil, fmt.Errorf("error fetching process template id %w", err) + } + + capabilities := map[string]map[string]string{ + "versioncontrol": { + "sourceControlType": "git", + }, + "processTemplate": { + "templateTypeId": processTemplateId, + }, + } + args := core.QueueCreateProjectArgs{ + ProjectToCreate: &core.TeamProject{ + Description: &description, + Name: &name, + Visibility: &core.ProjectVisibilityValues.Private, + Capabilities: &capabilities, + }, + } + res, err := coreClient.QueueCreateProject(ctx, args) + if err != nil { + return nil, err + } + + operationsClient := operations.NewClient(ctx, connection) + + getOperationsArgs := operations.GetOperationArgs{ + OperationId: res.Id, + } + + projectCreated := false + maxCheck := 10 + count := 0 + + for !projectCreated { + operation, err := operationsClient.GetOperation(ctx, getOperationsArgs) + if err != nil { + log.Fatal(err) + } + + if *operation.Status == "succeeded" { + projectCreated = true + } + + if count >= maxCheck { + log.Fatalf("Error creating project %s", name) + } + + count++ + time.Sleep(2 * time.Second) + } + + project, err := getAzdoProjectByName(ctx, connection, name) + if err != nil { + return nil, err + } + return project, nil +} + +func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azuredevops.Connection, env *environment.Environment, console input.Console) (string, string, error) { + var project *core.TeamProjectReference + currentFolderName := filepath.Base(repoPath) + var projectDescription string = "Azure Dev CLI Project" + + for { + name, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Enter the name for your new Azure Devops Project OR Hit enter to use this name:", + DefaultValue: currentFolderName, + }) + if err != nil { + return "", "", fmt.Errorf("asking for new repository name: %w", err) + } + var message string = "" + newProject, err := createAzdoProject(ctx, connection, name, projectDescription, console) + if err != nil { + message = err.Error() + } + if strings.Contains(message, fmt.Sprintf("The following project already exists on the Azure DevOps Server: %s", name)) { + console.Message(ctx, fmt.Sprintf("error: the project name '%s' is already in use\n", name)) + continue // try again + } else if strings.Contains(message, "The following name is not valid") { + console.Message(ctx, fmt.Sprintf("error: the project name '%s' is not a valid Azure DevOps project Name. See https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops#project-names\n", name)) + continue // try again + } else if err != nil { + return "", "", fmt.Errorf("creating repository: %w", err) + } else { + project = newProject + break + } + } + + return *project.Name, project.Id.String(), nil +} +func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connection, name string) (*core.TeamProjectReference, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return nil, err + } args := core.GetProjectsArgs{} getProjectsResponse, err := coreClient.GetProjects(ctx, args) if err != nil { - return "", err + return nil, err + } + + projects := getProjectsResponse.Value + for _, project := range projects { + if *project.Name == name { + return &project, nil + } + } + + return nil, fmt.Errorf("azure devops project %s not found", name) +} +func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, string, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return "", "", err + } + + args := core.GetProjectsArgs{} + getProjectsResponse, err := coreClient.GetProjects(ctx, args) + if err != nil { + return "", "", err } projects := getProjectsResponse.Value + projectsList := make([]core.TeamProjectReference, len(projects)) options := make([]string, len(projects)) for idx, project := range projects { options[idx] = *project.Name + projectsList[idx] = project } projectIdx, err := console.Select(ctx, input.ConsoleOptions{ @@ -123,8 +262,8 @@ func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Con }) if err != nil { - return "", fmt.Errorf("prompting for azdo project: %w", err) + return "", "", fmt.Errorf("prompting for azdo project: %w", err) } - return options[projectIdx], nil + return options[projectIdx], projectsList[projectIdx].Id.String(), nil } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 42b517d9d6f..9a1e299a4f9 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -30,6 +30,8 @@ type AzdoHubScmProvider struct { } type AzdoRepositoryDetails struct { projectName string + projectId string + repoId string orgName string repoName string remoteUrl string @@ -56,6 +58,15 @@ func (p *AzdoHubScmProvider) preConfigureCheck(ctx context.Context, console inpu return err } +func (p *AzdoHubScmProvider) saveEnvironmentConfig(key string, value string) error { + p.Env.Values[key] = value + err := p.Env.Save() + if err != nil { + return err + } + return nil +} + // name returns the name of the provider func (p *AzdoHubScmProvider) name() string { return "Azure DevOps" @@ -81,6 +92,17 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons repoDetails.repoName = *repo.Name repoDetails.remoteUrl = *repo.RemoteUrl repoDetails.sshUrl = *repo.SshUrl + repoDetails.repoId = repo.Id.String() + + err = p.saveEnvironmentConfig(AzDoEnvironmentRepoIdName, repoDetails.repoId) + if err != nil { + return "", fmt.Errorf("error saving repo id to environment %w", err) + } + + err = p.saveEnvironmentConfig(AzDoEnvironmentRepoName, repoDetails.repoName) + if err != nil { + return "", fmt.Errorf("error saving repo name to environment %w", err) + } remoteParts := strings.Split(repoDetails.remoteUrl, "@") if len(remoteParts) < 2 { @@ -127,9 +149,9 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop connection := getAzdoConnection(ctx, org, pat) return connection, nil } -func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, error) { +func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, string, error) { if p.repoDetails != nil && p.repoDetails.projectName != "" { - return p.repoDetails.projectName, nil + return p.repoDetails.projectName, p.repoDetails.projectId, nil } idx, err := console.Select(ctx, input.ConsoleOptions{ Message: "How would you like to configure your project?", @@ -140,41 +162,55 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in DefaultValue: "Create a new Azure DevOps Project", }) if err != nil { - return "", fmt.Errorf("prompting for azdo project type: %w", err) + return "", "", fmt.Errorf("prompting for azdo project type: %w", err) } connection, err := p.getAzdoConnection(ctx) if err != nil { - return "", err + return "", "", err } var projectName string + var projectId string switch idx { // Select from an existing AzDo project case 0: - name, err := getAzdoProjectFromExisting(ctx, connection, console) + projectName, projectId, err = getAzdoProjectFromExisting(ctx, connection, console) if err != nil { - return "", err + return "", "", err } - projectName = name // Create a new AzDo project case 1: - fmt.Println("New Azdo") + projectName, projectId, err = getAzdoProjectFromNew(ctx, p.AzdContext.ProjectDirectory(), connection, p.Env, console) + if err != nil { + return "", "", err + } default: panic(fmt.Sprintf("unexpected selection index %d", idx)) } - return projectName, nil + return projectName, projectId, nil } // configureGitRemote set up or create the git project and git remote func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath string, remoteName string, console input.Console) (string, error) { - projectName, err := p.ensureProjectExists(ctx, console) + projectName, projectId, err := p.ensureProjectExists(ctx, console) if err != nil { return "", err } repoDetails := p.getRepoDetails() repoDetails.projectName = projectName + repoDetails.projectId = projectId + + err = p.saveEnvironmentConfig(AzDoEnvironmentProjectIdName, projectId) + if err != nil { + return "", fmt.Errorf("error saving project id to environment %w", err) + } + + err = p.saveEnvironmentConfig(AzDoEnvironmentProjectName, projectName) + if err != nil { + return "", fmt.Errorf("error saving project name to environment %w", err) + } // There are a few ways to configure the remote so offer a choice to the user. idx, err := console.Select(ctx, input.ConsoleOptions{ @@ -193,7 +229,7 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st var remoteUrl string switch idx { - // Select from an existing GitHub project + // Select from an existing Azure Devops project case 0: remoteUrl, err = p.ensureGitRepositoryExists(ctx, console) if err != nil { @@ -244,13 +280,25 @@ func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl strin return nil, err } - inputConsole := input.GetConsole(ctx) - projectName, err := p.ensureProjectExists(ctx, inputConsole) - if err != nil { - return nil, err - } repoDetails := p.getRepoDetails() - repoDetails.projectName = projectName + if repoDetails.orgName == "" { + repoDetails.orgName = p.Env.Values[AzDoEnvironmentOrgName] + } + if repoDetails.projectName == "" { + repoDetails.projectName = p.Env.Values[AzDoEnvironmentProjectName] + } + if repoDetails.projectId == "" { + repoDetails.projectId = p.Env.Values[AzDoEnvironmentProjectIdName] + } + if repoDetails.repoName == "" { + repoDetails.repoName = p.Env.Values[AzDoEnvironmentRepoName] + } + if repoDetails.repoId == "" { + repoDetails.repoId = p.Env.Values[AzDoEnvironmentRepoIdName] + } + if repoDetails.remoteUrl == "" { + repoDetails.remoteUrl = remoteUrl + } return &gitRepositoryDetails{ owner: p.repoDetails.orgName, @@ -366,6 +414,5 @@ func (p *AzdoCiProvider) configureConnection( // configurePipeline create Azdo pipeline func (p *AzdoCiProvider) configurePipeline(ctx context.Context) error { - return nil } diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 52bda7a3620..c320dde088a 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -289,15 +289,15 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { if doPush { err = manager.pushGitRepo(ctx, currentBranch) - if err != nil { - return err - } manager.ScmProvider.postGitPush( ctx, gitRepoInfo, manager.PipelineRemoteName, currentBranch, inputConsole) + if err != nil { + return err + } } else { inputConsole.Message(ctx, fmt.Sprintf( From ffe676f6c9d852e0574383197fe25aa7f2d09de2 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Thu, 15 Sep 2022 01:43:53 -0700 Subject: [PATCH 08/33] Cleanup and speed up azdo project creation --- cli/azd/pkg/commands/pipeline/azdo.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 5ee172d42c3..9882e6447c0 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -3,7 +3,6 @@ package pipeline import ( "context" "fmt" - "log" "os" "path/filepath" "strings" @@ -52,7 +51,6 @@ func ensureAzdoPatExists(ctx context.Context, env *environment.Environment) (str func ensureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") - } func getAzdoConnection(ctx context.Context, organization string, personalAccessToken string) *azuredevops.Connection { @@ -100,13 +98,12 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or } } - return nil, fmt.Errorf("Error finding git repository %s in organization %s", selectedRepoName, orgName) + return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) } func GetProcessTemplateId(ctx context.Context, client core.Client) (string, error) { processArgs := core.GetProcessesArgs{} processes, err := client.GetProcesses(ctx, processArgs) - _ = processes if err != nil { return "", err } @@ -159,7 +156,7 @@ func createAzdoProject(ctx context.Context, connection *azuredevops.Connection, for !projectCreated { operation, err := operationsClient.GetOperation(ctx, getOperationsArgs) if err != nil { - log.Fatal(err) + return nil, err } if *operation.Status == "succeeded" { @@ -167,11 +164,11 @@ func createAzdoProject(ctx context.Context, connection *azuredevops.Connection, } if count >= maxCheck { - log.Fatalf("Error creating project %s", name) + return nil, fmt.Errorf("error creating azure devops project %s", name) } count++ - time.Sleep(2 * time.Second) + time.Sleep(700 * time.Millisecond) } project, err := getAzdoProjectByName(ctx, connection, name) From 86b15d117964f5db065f28c001b9c7b51925fdc4 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Thu, 15 Sep 2022 11:21:11 -0700 Subject: [PATCH 09/33] Utilizing default repo when creating a new project --- cli/azd/pkg/commands/pipeline/azdo.go | 37 +++++++++++- .../pkg/commands/pipeline/azdo_provider.go | 57 ++++++++++++++----- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 9882e6447c0..8da2802eef6 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -59,6 +59,35 @@ func getAzdoConnection(ctx context.Context, organization string, personalAccessT return connection } +func getAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName string, connection *azuredevops.Connection) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + includeLinks := true + includeAllUrls := true + repoArgs := git.GetRepositoriesArgs{ + Project: &projectName, + IncludeLinks: &includeLinks, + IncludeAllUrls: &includeAllUrls, + } + + getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) + if err != nil { + return nil, err + } + repos := *getRepositoriesResult + + for _, repo := range repos { + if *repo.Name == projectName { + return &repo, nil + } + } + + return nil, fmt.Errorf("error finding default git repository %s in project %s", projectName) +} + func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { gitClient, err := git.NewClient(ctx, connection) if err != nil { @@ -101,7 +130,7 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) } -func GetProcessTemplateId(ctx context.Context, client core.Client) (string, error) { +func getProcessTemplateId(ctx context.Context, client core.Client) (string, error) { processArgs := core.GetProcessesArgs{} processes, err := client.GetProcesses(ctx, processArgs) if err != nil { @@ -117,7 +146,7 @@ func createAzdoProject(ctx context.Context, connection *azuredevops.Connection, return nil, err } - processTemplateId, err := GetProcessTemplateId(ctx, coreClient) + processTemplateId, err := getProcessTemplateId(ctx, coreClient) if err != nil { return nil, fmt.Errorf("error fetching process template id %w", err) } @@ -189,7 +218,7 @@ func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu DefaultValue: currentFolderName, }) if err != nil { - return "", "", fmt.Errorf("asking for new repository name: %w", err) + return "", "", fmt.Errorf("asking for new project name: %w", err) } var message string = "" newProject, err := createAzdoProject(ctx, connection, name, projectDescription, console) @@ -212,6 +241,7 @@ func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu return *project.Name, project.Id.String(), nil } + func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connection, name string) (*core.TeamProjectReference, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { @@ -233,6 +263,7 @@ func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connectio return nil, fmt.Errorf("azure devops project %s not found", name) } + func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, string, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 9a1e299a4f9..d60f429ac83 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -149,9 +149,9 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop connection := getAzdoConnection(ctx, org, pat) return connection, nil } -func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, string, error) { +func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, string, bool, error) { if p.repoDetails != nil && p.repoDetails.projectName != "" { - return p.repoDetails.projectName, p.repoDetails.projectId, nil + return p.repoDetails.projectName, p.repoDetails.projectId, false, nil } idx, err := console.Select(ctx, input.ConsoleOptions{ Message: "How would you like to configure your project?", @@ -162,38 +162,40 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in DefaultValue: "Create a new Azure DevOps Project", }) if err != nil { - return "", "", fmt.Errorf("prompting for azdo project type: %w", err) + return "", "", false, fmt.Errorf("prompting for azdo project type: %w", err) } connection, err := p.getAzdoConnection(ctx) if err != nil { - return "", "", err + return "", "", false, err } var projectName string var projectId string + var newProject bool = false switch idx { // Select from an existing AzDo project case 0: projectName, projectId, err = getAzdoProjectFromExisting(ctx, connection, console) if err != nil { - return "", "", err + return "", "", false, err } // Create a new AzDo project case 1: projectName, projectId, err = getAzdoProjectFromNew(ctx, p.AzdContext.ProjectDirectory(), connection, p.Env, console) + newProject = true if err != nil { - return "", "", err + return "", "", false, err } default: panic(fmt.Sprintf("unexpected selection index %d", idx)) } - return projectName, projectId, nil + return projectName, projectId, newProject, nil } // configureGitRemote set up or create the git project and git remote func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath string, remoteName string, console input.Console) (string, error) { - projectName, projectId, err := p.ensureProjectExists(ctx, console) + projectName, projectId, newProject, err := p.ensureProjectExists(ctx, console) if err != nil { return "", err } @@ -211,7 +213,40 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st if err != nil { return "", fmt.Errorf("error saving project name to environment %w", err) } + var remoteUrl string + + if !newProject { + remoteUrl, err = p.promptForAzdoRepository(ctx, console) + if err != nil { + return "", err + } + } else { + remoteUrl, err = p.gethDefaultRepoRemote(ctx, projectName, projectId, console) + if err != nil { + return "", err + } + } + return remoteUrl, nil +} + +func (p *AzdoHubScmProvider) gethDefaultRepoRemote(ctx context.Context, projectName string, projectId string, console input.Console) (string, error) { + connection, err := p.getAzdoConnection(ctx) + if err != nil { + return "", err + } + repo, err := getAzDoDefaultGitRepositoriesInProject(ctx, projectName, connection) + if err != nil { + return "", err + } + + message := fmt.Sprintf("using default repo (%s) in newly created project(%s)", *repo.Name, projectName) + fmt.Println(message) + return *repo.RemoteUrl, nil +} + +func (p *AzdoHubScmProvider) promptForAzdoRepository(ctx context.Context, console input.Console) (string, error) { + var remoteUrl string // There are a few ways to configure the remote so offer a choice to the user. idx, err := console.Select(ctx, input.ConsoleOptions{ Message: fmt.Sprintf("How would you like to configure your remote? (Organization: %s)", p.repoDetails.projectName), @@ -226,8 +261,6 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st return "", fmt.Errorf("prompting for remote configuration type: %w", err) } - var remoteUrl string - switch idx { // Select from an existing Azure Devops project case 0: @@ -239,9 +272,7 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st // Create a new project case 1: fmt.Println("New") - // Enter a URL directly. - case 2: - fmt.Println("Prompt") + default: panic(fmt.Sprintf("unexpected selection index %d", idx)) } From 47e7d1b67ea4de172145856696297f3f7081809f Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Thu, 15 Sep 2022 14:54:08 -0700 Subject: [PATCH 10/33] Cleanup + support for create new repos on existing projects. --- cli/azd/pkg/commands/pipeline/azdo.go | 27 +++- .../pkg/commands/pipeline/azdo_provider.go | 115 +++++++++++++++--- cli/azd/pkg/commands/pipeline/pipeline.go | 2 + .../pkg/commands/pipeline/pipeline_manager.go | 3 + 4 files changed, 126 insertions(+), 21 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 8da2802eef6..b282dd2a75b 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -26,6 +26,8 @@ var ( AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" + AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" + AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" ) type AzDoClient struct { @@ -130,6 +132,27 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) } +func createRepository(ctx context.Context, projectId string, repoName string, connection *azuredevops.Connection) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + gitRepositoryCreateOptions := git.GitRepositoryCreateOptions{ + Name: &repoName, + } + + createRepositoryArgs := git.CreateRepositoryArgs{ + Project: &projectId, + GitRepositoryToCreate: &gitRepositoryCreateOptions, + } + repo, err := gitClient.CreateRepository(ctx, createRepositoryArgs) + if err != nil { + return nil, err + } + return repo, nil +} + func getProcessTemplateId(ctx context.Context, client core.Client) (string, error) { processArgs := core.GetProcessesArgs{} processes, err := client.GetProcesses(ctx, processArgs) @@ -140,7 +163,7 @@ func getProcessTemplateId(ctx context.Context, client core.Client) (string, erro return fmt.Sprintf("%s", process.Id), nil } -func createAzdoProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { +func createProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { return nil, err @@ -221,7 +244,7 @@ func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu return "", "", fmt.Errorf("asking for new project name: %w", err) } var message string = "" - newProject, err := createAzdoProject(ctx, connection, name, projectDescription, console) + newProject, err := createProject(ctx, connection, name, projectDescription, console) if err != nil { message = err.Error() } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index d60f429ac83..50389efd282 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -15,9 +15,11 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/azure/azure-dev/cli/azd/pkg/tools/git" "github.com/microsoft/azure-devops-go-api/azuredevops" + azdoGit "github.com/microsoft/azure-devops-go-api/azuredevops/git" ) // AzdoHubScmProvider implements ScmProvider using Azure DevOps as the provider @@ -34,8 +36,10 @@ type AzdoRepositoryDetails struct { repoId string orgName string repoName string + repoWebUrl string remoteUrl string sshUrl string + pushSuccess bool } // *** subareaProvider implementation ****** @@ -73,38 +77,95 @@ func (p *AzdoHubScmProvider) name() string { } // *** scmProvider implementation ****** -func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, console input.Console) (string, error) { - if p.repoDetails != nil && p.repoDetails.repoName != "" { - return p.repoDetails.remoteUrl, nil +func (p *AzdoHubScmProvider) StoreRepoDetails(ctx context.Context, repo *azdoGit.GitRepository) error { + repoDetails := p.getRepoDetails() + repoDetails.repoName = *repo.Name + repoDetails.remoteUrl = *repo.RemoteUrl + repoDetails.repoWebUrl = *repo.WebUrl + repoDetails.sshUrl = *repo.SshUrl + repoDetails.repoId = repo.Id.String() + + err := p.saveEnvironmentConfig(AzDoEnvironmentRepoIdName, p.repoDetails.repoId) + if err != nil { + return fmt.Errorf("error saving repo id to environment %w", err) + } + + err = p.saveEnvironmentConfig(AzDoEnvironmentRepoName, p.repoDetails.repoName) + if err != nil { + return fmt.Errorf("error saving repo name to environment %w", err) + } + + err = p.saveEnvironmentConfig(AzDoEnvironmentRepoWebUrl, p.repoDetails.repoWebUrl) + if err != nil { + return fmt.Errorf("error saving repo web url to environment %w", err) } + return nil +} + +func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context, console input.Console) (string, error) { connection, err := p.getAzdoConnection(ctx) if err != nil { return "", err } - repo, err := getAzDoGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console) + var repo *azdoGit.GitRepository + for { + name, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Enter the name for your new Azure Devops Repository OR Hit enter to use this name:", + DefaultValue: p.repoDetails.projectName, + }) + if err != nil { + return "", fmt.Errorf("asking for new project name: %w", err) + } + + var message string + newRepo, err := createRepository(ctx, p.repoDetails.projectId, name, connection) + if err != nil { + message = err.Error() + } + if strings.Contains(message, fmt.Sprintf("A Git repository with the name %s already exists.", name)) { + console.Message(ctx, fmt.Sprintf("error: the repo name '%s' is already in use\n", name)) + continue // try again + } else if strings.Contains(message, "TF401025: 'repoName' is not a valid name for a Git repository.") { + console.Message(ctx, fmt.Sprintf("error: '%s' is not a valid Azure Devops repo name. See https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops#azure-repos-git\n", name)) + continue // try again + } else if err != nil { + return "", fmt.Errorf("creating repository: %w", err) + } else { + repo = newRepo + break + } + } + + err = p.StoreRepoDetails(ctx, repo) if err != nil { return "", err + + } + return *repo.RemoteUrl, nil +} +func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, console input.Console) (string, error) { + if p.repoDetails != nil && p.repoDetails.repoName != "" { + return p.repoDetails.remoteUrl, nil } - repoDetails := p.getRepoDetails() - repoDetails.repoName = *repo.Name - repoDetails.remoteUrl = *repo.RemoteUrl - repoDetails.sshUrl = *repo.SshUrl - repoDetails.repoId = repo.Id.String() + connection, err := p.getAzdoConnection(ctx) + if err != nil { + return "", err + } - err = p.saveEnvironmentConfig(AzDoEnvironmentRepoIdName, repoDetails.repoId) + repo, err := getAzDoGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console) if err != nil { - return "", fmt.Errorf("error saving repo id to environment %w", err) + return "", err } - err = p.saveEnvironmentConfig(AzDoEnvironmentRepoName, repoDetails.repoName) + err = p.StoreRepoDetails(ctx, repo) if err != nil { - return "", fmt.Errorf("error saving repo name to environment %w", err) + return "", err } - remoteParts := strings.Split(repoDetails.remoteUrl, "@") + remoteParts := strings.Split(p.repoDetails.remoteUrl, "@") if len(remoteParts) < 2 { return "", fmt.Errorf("invalid azure devops remote") } @@ -114,7 +175,9 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons if err != nil { return "", err } + updatedRemote := fmt.Sprintf("%s:%s@%s", remoteUser, pat, remoteHost) + console.Message(ctx, fmt.Sprintf("using azure devOps repo: %s", p.repoDetails.repoWebUrl)) return updatedRemote, nil } @@ -221,7 +284,7 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st return "", err } } else { - remoteUrl, err = p.gethDefaultRepoRemote(ctx, projectName, projectId, console) + remoteUrl, err = p.getDefaultRepoRemote(ctx, projectName, projectId, console) if err != nil { return "", err } @@ -229,7 +292,7 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st return remoteUrl, nil } -func (p *AzdoHubScmProvider) gethDefaultRepoRemote(ctx context.Context, projectName string, projectId string, console input.Console) (string, error) { +func (p *AzdoHubScmProvider) getDefaultRepoRemote(ctx context.Context, projectName string, projectId string, console input.Console) (string, error) { connection, err := p.getAzdoConnection(ctx) if err != nil { return "", err @@ -239,8 +302,13 @@ func (p *AzdoHubScmProvider) gethDefaultRepoRemote(ctx context.Context, projectN return "", err } + err = p.StoreRepoDetails(ctx, repo) + if err != nil { + return "", err + } + message := fmt.Sprintf("using default repo (%s) in newly created project(%s)", *repo.Name, projectName) - fmt.Println(message) + console.Message(ctx, message) return *repo.RemoteUrl, nil } @@ -271,7 +339,10 @@ func (p *AzdoHubScmProvider) promptForAzdoRepository(ctx context.Context, consol // Create a new project case 1: - fmt.Println("New") + remoteUrl, err = p.createNewGitRepositoryFromInput(ctx, console) + if err != nil { + return "", err + } default: panic(fmt.Sprintf("unexpected selection index %d", idx)) @@ -327,6 +398,9 @@ func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl strin if repoDetails.repoId == "" { repoDetails.repoId = p.Env.Values[AzDoEnvironmentRepoIdName] } + if repoDetails.repoWebUrl == "" { + repoDetails.repoWebUrl = p.Env.Values[AzDoEnvironmentRepoWebUrl] + } if repoDetails.remoteUrl == "" { repoDetails.remoteUrl = remoteUrl } @@ -352,7 +426,7 @@ func (p *AzdoHubScmProvider) preventGitPush( return false, err } if !foundHelper { - fmt.Println(` + console.Message(ctx, ` A credential helper is not configured for git. This will require you to enter your Azure DevOps PAT when executing a git push. https://git-scm.com/docs/git-credential-store @@ -396,6 +470,9 @@ func (p *AzdoHubScmProvider) postGitPush( //Reset remote to original url without PAT gitCli.UpdateRemote(ctx, p.AzdContext.ProjectDirectory(), remoteName, p.repoDetails.remoteUrl) + if gitRepo.pushStatus { + console.Message(ctx, output.WithSuccessFormat(AzdoConfigSuccessMessage, p.repoDetails.repoWebUrl)) + } return false, nil } diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index c814e1c2512..84c450e60f5 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -34,6 +34,8 @@ type gitRepositoryDetails struct { repoName string // System path where the git project is gitProjectPath string + //Indicates if the repo was successfully pushed a remote + pushStatus bool details interface{} } diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index c320dde088a..d6af99920d5 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -289,6 +289,9 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { if doPush { err = manager.pushGitRepo(ctx, currentBranch) + if err == nil { + gitRepoInfo.pushStatus = true + } manager.ScmProvider.postGitPush( ctx, gitRepoInfo, From 33a3e9157fe20f1b4ff4cb2050f97156e0ef1065 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 15:35:07 -0700 Subject: [PATCH 11/33] Adding Pipeline creation for Azure DevOps Co-authored-by: Hadwa Gaber --- cli/azd/pkg/commands/pipeline/azdo.go | 82 +++++++++++++++++-- .../pkg/commands/pipeline/azdo_provider.go | 39 ++++++++- .../pkg/commands/pipeline/github_provider.go | 2 +- cli/azd/pkg/commands/pipeline/pipeline.go | 2 +- .../pkg/commands/pipeline/pipeline_manager.go | 12 +-- 5 files changed, 122 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index b282dd2a75b..ac0de5aebc6 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -2,6 +2,7 @@ package pipeline import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -12,22 +13,24 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/build" "github.com/microsoft/azure-devops-go-api/azuredevops/core" "github.com/microsoft/azure-devops-go-api/azuredevops/git" "github.com/microsoft/azure-devops-go-api/azuredevops/operations" ) var ( - // The hostname of the AzDo PaaS service. - AzDoHostName = "dev.azure.com" - AzDoPatName = "AZURE_DEVOPS_EXT_PAT" - AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" + AzDoHostName = "dev.azure.com" // hostname of the AzDo PaaS service. + AzDoPatName = "AZURE_DEVOPS_EXT_PAT" // environment variable that holds the Azure DevOps PAT + AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" // environment variable that holds the Azure DevOps Organization Name AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" + AzurePipelineName = "Azure Dev Deploy" + AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" ) type AzDoClient struct { @@ -255,7 +258,7 @@ func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu console.Message(ctx, fmt.Sprintf("error: the project name '%s' is not a valid Azure DevOps project Name. See https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops#project-names\n", name)) continue // try again } else if err != nil { - return "", "", fmt.Errorf("creating repository: %w", err) + return "", "", fmt.Errorf("creating project: %w", err) } else { project = newProject break @@ -318,3 +321,72 @@ func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Con return options[projectIdx], projectsList[projectIdx].Id.String(), nil } + +func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bool) build.BuildDefinitionVariable { + return build.BuildDefinitionVariable{ + AllowOverride: &allowOverride, + IsSecret: &isSecret, + Value: &value, + } +} + +func createPipeline( + ctx context.Context, + projectId string, + name string, + repoName string, + connection *azuredevops.Connection, + credentials AzureServicePrincipalCredentials, + env environment.Environment) error { + + client, err := build.NewClient(ctx, connection) + if err != nil { + return err + } + repoType := "tfsgit" + buildDefinitionType := build.DefinitionType("build") + definitionQueueStatus := build.DefinitionQueueStatus("enabled") + buildRepository := &build.BuildRepository{ + Type: &repoType, + Name: &repoName, + } + + process := make(map[string]interface{}) + process["type"] = 2 + process["yamlFilename"] = AzurePipelineYamlPath + + variables := make(map[string]build.BuildDefinitionVariable) + variables["AZURE_SUBSCRIPTION_ID"] = createBuildDefinitionVariable(credentials.SubscriptionId, false, false) + variables["ARM_TENANT_ID"] = createBuildDefinitionVariable(credentials.TenantId, false, false) + variables["ARM_CLIENT_ID"] = createBuildDefinitionVariable(credentials.ClientId, true, false) + variables["ARM_CLIENT_SECRET"] = createBuildDefinitionVariable(credentials.ClientSecret, true, false) + variables["AZURE_LOCATION"] = createBuildDefinitionVariable(env.GetLocation(), false, false) + variables["AZURE_ENV_NAME"] = createBuildDefinitionVariable(env.GetEnvName(), false, false) + + buildDefinition := &build.BuildDefinition{ + Name: &name, + Type: &buildDefinitionType, + QueueStatus: &definitionQueueStatus, + Repository: buildRepository, + Process: process, + Variables: &variables, + } + + createDefinitionArgs := &build.CreateDefinitionArgs{ + Project: &projectId, + Definition: buildDefinition, + } + body, marshalErr := json.Marshal(*createDefinitionArgs.Definition) + if marshalErr != nil { + return marshalErr + } + json := string(body) + _ = json + newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) + if err != nil { + return err + } + _ = newBuildDefinition + + return nil +} diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 50389efd282..4b0f644fed6 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -480,7 +480,7 @@ func (p *AzdoHubScmProvider) postGitPush( type AzdoCiProvider struct { Env *environment.Environment AzdContext *azdcontext.AzdContext - ScmProvider *ScmProvider + credentials *AzureServicePrincipalCredentials } // *** subareaProvider implementation ****** @@ -517,10 +517,45 @@ func (p *AzdoCiProvider) configureConnection( credentials json.RawMessage, console input.Console) error { + azureCredentials, err := parseCredentials(ctx, credentials) + if err != nil { + return err + } + + p.credentials = azureCredentials return nil } +type AzureServicePrincipalCredentials struct { + TenantId string `json:"tenantId"` + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + SubscriptionId string `json:"subscriptionId"` +} + +func parseCredentials(ctx context.Context, credentials json.RawMessage) (*AzureServicePrincipalCredentials, error) { + azureCredentials := AzureServicePrincipalCredentials{} + if e := json.Unmarshal(credentials, &azureCredentials); e != nil { + return nil, fmt.Errorf("setting terraform env var credentials: %w", e) + } + return &azureCredentials, nil +} + // configurePipeline create Azdo pipeline -func (p *AzdoCiProvider) configurePipeline(ctx context.Context) error { +func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error { + details := repoDetails.details.(*AzdoRepositoryDetails) + + org, err := ensureAzdoOrgNameExists(ctx, p.Env) + if err != nil { + return err + } + pat, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return err + } + + connection := getAzdoConnection(ctx, org, pat) + + createPipeline(ctx, details.projectId, AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) return nil } diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index 1f144f5e7f7..8f4c568031b 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -364,7 +364,7 @@ func (p *GitHubCiProvider) configureConnection( // configurePipeline is a no-op for GitHub, as the pipeline is automatically // created by creating the workflow files in .github folder. -func (p *GitHubCiProvider) configurePipeline(ctx context.Context) error { +func (p *GitHubCiProvider) configurePipeline(ctx context.Context, repoDetails gitRepositoryDetails) error { return nil } diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 84c450e60f5..d3fd1fe91af 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -71,7 +71,7 @@ type CiProvider interface { // compose the behavior from subareaProvider subareaProvider // configurePipeline set up or create the CI pipeline. - configurePipeline(ctx context.Context) error + configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error // configureConnection use the credential to set up the connection from the pipeline // to Azure configureConnection( diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index d6af99920d5..013460bc5f4 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -229,12 +229,6 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { return fmt.Errorf("ensuring git remote: %w", err) } - // config pipeline handles setting or creating the provider pipeline to be used - err = manager.CiProvider.configurePipeline(ctx) - if err != nil { - return err - } - // Figure out what is the expected provider to use for provisioning prj, err := project.LoadProjectConfig(manager.AzdCtx.ProjectPath(), &manager.Environment) if err != nil { @@ -252,6 +246,12 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { return err } + // config pipeline handles setting or creating the provider pipeline to be used + err = manager.CiProvider.configurePipeline(ctx, gitRepoInfo) + if err != nil { + return err + } + var doPush bool = true if existingSP == "" { // The CI pipeline should be set-up and ready at this point. From c5d47782ea1ece3d830b1feaeb7b9a1e06e31466 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 17 Sep 2022 02:33:10 +0000 Subject: [PATCH 12/33] build --- cli/azd/pkg/commands/pipeline/github_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index 14ead1d366c..c111edcba67 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -380,7 +380,7 @@ func (p *GitHubCiProvider) configureConnection( // configurePipeline is a no-op for GitHub, as the pipeline is automatically // created by creating the workflow files in .github folder. -func (p *GitHubCiProvider) configurePipeline(ctx context.Context, repoDetails gitRepositoryDetails) error { +func (p *GitHubCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error { return nil } From ac6248967acaf9f0a885a96906723c09a5d788e3 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 20:01:06 -0700 Subject: [PATCH 13/33] adding create pipeline, service connection and build policy Co-authored-by: Hadwa Gaber --- cli/azd/pkg/commands/pipeline/azdo.go | 269 +++++++++++++++++- .../pkg/commands/pipeline/azdo_provider.go | 55 +++- 2 files changed, 301 insertions(+), 23 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index ac0de5aebc6..7aba2ff94ff 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -2,7 +2,6 @@ package pipeline import ( "context" - "encoding/json" "fmt" "os" "path/filepath" @@ -17,6 +16,9 @@ import ( "github.com/microsoft/azure-devops-go-api/azuredevops/core" "github.com/microsoft/azure-devops-go-api/azuredevops/git" "github.com/microsoft/azure-devops-go-api/azuredevops/operations" + "github.com/microsoft/azure-devops-go-api/azuredevops/policy" + "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" + "github.com/microsoft/azure-devops-go-api/azuredevops/taskagent" ) var ( @@ -31,6 +33,8 @@ var ( AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" AzurePipelineName = "Azure Dev Deploy" AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" + CloudEnvironment = "AzureCloud" + DefaultBranch = "master" ) type AzDoClient struct { @@ -330,6 +334,26 @@ func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bo } } +func getAgentQueue(ctx context.Context, projectId string, connection *azuredevops.Connection) (*taskagent.TaskAgentQueue, error) { + client, err := taskagent.NewClient(ctx, connection) + if err != nil { + return nil, err + } + getAgentQueuesArgs := taskagent.GetAgentQueuesArgs{ + Project: &projectId, + } + queues, err := client.GetAgentQueues(ctx, getAgentQueuesArgs) + if err != nil { + return nil, err + } + for _, queue := range *queues { + if *queue.Name == "Default" { + return &queue, nil + } + } + return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) +} + func createPipeline( ctx context.Context, projectId string, @@ -337,18 +361,21 @@ func createPipeline( repoName string, connection *azuredevops.Connection, credentials AzureServicePrincipalCredentials, - env environment.Environment) error { + env environment.Environment) (*build.BuildDefinition, error) { client, err := build.NewClient(ctx, connection) if err != nil { - return err + return nil, err } + repoType := "tfsgit" buildDefinitionType := build.DefinitionType("build") definitionQueueStatus := build.DefinitionQueueStatus("enabled") + defaultBranch := fmt.Sprintf("refs/heads/%s", DefaultBranch) buildRepository := &build.BuildRepository{ - Type: &repoType, - Name: &repoName, + Type: &repoType, + Name: &repoName, + DefaultBranch: &defaultBranch, } process := make(map[string]interface{}) @@ -363,30 +390,248 @@ func createPipeline( variables["AZURE_LOCATION"] = createBuildDefinitionVariable(env.GetLocation(), false, false) variables["AZURE_ENV_NAME"] = createBuildDefinitionVariable(env.GetEnvName(), false, false) + queue, err := getAgentQueue(ctx, projectId, connection) + if err != nil { + return nil, err + } + + agentPoolQueue := &build.AgentPoolQueue{ + Id: queue.Id, + Name: queue.Name, + } + + trigger := make(map[string]interface{}) + trigger["batchChanges"] = false + trigger["maxConcurrentBuildsPerBranch"] = 1 + trigger["pollingInterval"] = 0 + trigger["isSettingsSourceOptionSupported"] = true + trigger["defaultSettingsSourceType"] = 2 + trigger["settingsSourceType"] = 2 + trigger["defaultSettingsSourceType"] = 2 + trigger["triggerType"] = 2 + + triggers := make([]interface{}, 1) + triggers[0] = trigger + buildDefinition := &build.BuildDefinition{ Name: &name, Type: &buildDefinitionType, QueueStatus: &definitionQueueStatus, Repository: buildRepository, Process: process, + Queue: agentPoolQueue, Variables: &variables, + Triggers: &triggers, } createDefinitionArgs := &build.CreateDefinitionArgs{ Project: &projectId, Definition: buildDefinition, } - body, marshalErr := json.Marshal(*createDefinitionArgs.Definition) - if marshalErr != nil { - return marshalErr - } - json := string(body) - _ = json + newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) + if err != nil { + return nil, err + } + + return newBuildDefinition, nil +} + +func queueBuild( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + buildDefinition *build.BuildDefinition) error { + client, err := build.NewClient(ctx, connection) + if err != nil { + return err + } + definitionReference := &build.DefinitionReference{ + Id: buildDefinition.Id, + } + + newBuild := &build.Build{ + Definition: definitionReference, + } + queueBuildArgs := build.QueueBuildArgs{ + Project: &projectId, + Build: newBuild, + } + + //time.Sleep(500 * time.Millisecond) + + _, err = client.QueueBuild(ctx, queueBuildArgs) + if err != nil { + return err + } + + return nil +} +func authorizeServiceConnectionToAllPipelines( + ctx context.Context, + projectId string, + endpoint *serviceendpoint.ServiceEndpoint, + connection *azuredevops.Connection) error { + buildClient, err := build.NewClient(ctx, connection) + if err != nil { + return err + } + + endpointResource := "endpoint" + endpointAuthorized := true + endpointId := endpoint.Id.String() + resources := make([]build.DefinitionResourceReference, 1) + resources[0] = build.DefinitionResourceReference{ + Type: &endpointResource, + Authorized: &endpointAuthorized, + Id: &endpointId, + } + + authorizeProjectResourcesArgs := build.AuthorizeProjectResourcesArgs{ + Project: &projectId, + Resources: &resources, + } + + _, err = buildClient.AuthorizeProjectResources(ctx, authorizeProjectResourcesArgs) + + if err != nil { + return err + } + return nil +} + +func createServiceConnection( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + azdEnvironment environment.Environment, + repoDetails *gitRepositoryDetails, + credentials AzureServicePrincipalCredentials, + console input.Console) error { + + client, err := serviceendpoint.NewClient(ctx, connection) + if err != nil { + return err + } + + endpointType := "azurerm" + endpointOwner := "library" + endpointUrl := "https://management.azure.com/" + endpointName := "azconnection" + endpointIsShared := false + endpointScheme := "ServicePrincipal" + + endpointAuthorizationParameters := make(map[string]string) + endpointAuthorizationParameters["serviceprincipalid"] = credentials.ClientId + endpointAuthorizationParameters["serviceprincipalkey"] = credentials.ClientSecret + endpointAuthorizationParameters["authenticationType"] = "spnKey" + endpointAuthorizationParameters["tenantid"] = credentials.TenantId + + endpointData := make(map[string]string) + endpointData["environment"] = CloudEnvironment + endpointData["subscriptionId"] = credentials.SubscriptionId + endpointData["subscriptionName"] = "azure subscription" + endpointData["scopeLevel"] = "Subscription" + endpointData["creationMode"] = "Manual" + + endpointAuthorization := serviceendpoint.EndpointAuthorization{ + Scheme: &endpointScheme, + Parameters: &endpointAuthorizationParameters, + } + serviceEndpoint := &serviceendpoint.ServiceEndpoint{ + Type: &endpointType, + Owner: &endpointOwner, + Url: &endpointUrl, + Name: &endpointName, + IsShared: &endpointIsShared, + Authorization: &endpointAuthorization, + Data: &endpointData, + } + createServiceEndpointArgs := serviceendpoint.CreateServiceEndpointArgs{ + Project: &projectId, + Endpoint: serviceEndpoint, + } + + endpoint, err := client.CreateServiceEndpoint(ctx, createServiceEndpointArgs) + if err != nil { + return err + } + + authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) + + return nil +} + +func getBuildType(ctx context.Context, projectId *string, policyClient policy.Client) (*policy.PolicyType, error) { + getPolicyTypesArgs := policy.GetPolicyTypesArgs{ + Project: projectId, + } + policyTypes, err := policyClient.GetPolicyTypes(ctx, getPolicyTypesArgs) + if err != nil { + return nil, err + } + for _, policy := range *policyTypes { + if *policy.DisplayName == "Build" { + return &policy, nil + } + } + return nil, fmt.Errorf("could not find 'Build' policy type in project") +} + +func createBuildPolicy( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + repoId string, + buildDefinition *build.BuildDefinition) error { + client, err := policy.NewClient(ctx, connection) if err != nil { return err } - _ = newBuildDefinition + + buildPolicyType, err := getBuildType(ctx, &projectId, client) + if err != nil { + return err + } + + policyTypeRef := &policy.PolicyTypeRef{ + Id: buildPolicyType.Id, + } + policyRevision := 1 + policyIsDeleted := false + policyIsBlocking := true + policyIsEnabled := true + + policySettingsScope := make(map[string]interface{}) + policySettingsScope["repositoryId"] = repoId + policySettingsScope["refName"] = fmt.Sprintf("refs/heads/%s", DefaultBranch) + policySettingsScope["matchKind"] = "Exact" + + policySettingsScopes := make([]map[string]interface{}, 1) + policySettingsScopes[0] = policySettingsScope + + policySettings := make(map[string]interface{}) + policySettings["buildDefinitionId"] = buildDefinition.Id + policySettings["displayName"] = "Azure Dev Deploy PR" + policySettings["manualQueueOnly"] = false + policySettings["queueOnSourceUpdateOnly"] = true + policySettings["validDuration"] = 720 + policySettings["scope"] = policySettingsScopes + + policyConfiguration := &policy.PolicyConfiguration{ + Type: policyTypeRef, + Revision: &policyRevision, + IsDeleted: &policyIsDeleted, + IsBlocking: &policyIsBlocking, + IsEnabled: &policyIsEnabled, + Settings: policySettings, + } + + createPolicyConfigurationArgs := policy.CreatePolicyConfigurationArgs{ + Project: &projectId, + Configuration: policyConfiguration, + } + client.CreatePolicyConfiguration(ctx, createPolicyConfigurationArgs) return nil } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 4b0f644fed6..4ec79164c11 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -19,6 +19,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/azure/azure-dev/cli/azd/pkg/tools/git" "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/build" azdoGit "github.com/microsoft/azure-devops-go-api/azuredevops/git" ) @@ -31,15 +32,16 @@ type AzdoHubScmProvider struct { azdoConnection *azuredevops.Connection } type AzdoRepositoryDetails struct { - projectName string - projectId string - repoId string - orgName string - repoName string - repoWebUrl string - remoteUrl string - sshUrl string - pushSuccess bool + projectName string + projectId string + repoId string + orgName string + repoName string + repoWebUrl string + remoteUrl string + sshUrl string + pushSuccess bool + buildDefinition *build.BuildDefinition } // *** subareaProvider implementation ****** @@ -473,6 +475,22 @@ func (p *AzdoHubScmProvider) postGitPush( if gitRepo.pushStatus { console.Message(ctx, output.WithSuccessFormat(AzdoConfigSuccessMessage, p.repoDetails.repoWebUrl)) } + + connection, err := p.getAzdoConnection(ctx) + if err != nil { + return false, err + } + + err = createBuildPolicy(ctx, connection, p.repoDetails.projectId, p.repoDetails.repoId, p.repoDetails.buildDefinition) + if err != nil { + return false, err + } + + err = queueBuild(ctx, connection, p.repoDetails.projectId, p.repoDetails.buildDefinition) + if err != nil { + return false, err + } + return false, nil } @@ -523,6 +541,18 @@ func (p *AzdoCiProvider) configureConnection( } p.credentials = azureCredentials + details := repoDetails.details.(*AzdoRepositoryDetails) + org, err := ensureAzdoOrgNameExists(ctx, p.Env) + if err != nil { + return err + } + pat, err := ensureAzdoPatExists(ctx, p.Env) + if err != nil { + return err + } + connection := getAzdoConnection(ctx, org, pat) + + createServiceConnection(ctx, connection, details.projectId, *p.Env, repoDetails, *p.credentials, console) return nil } @@ -553,9 +583,12 @@ func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *git if err != nil { return err } - connection := getAzdoConnection(ctx, org, pat) - createPipeline(ctx, details.projectId, AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) + buildDefinition, err := createPipeline(ctx, details.projectId, AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) + if err != nil { + return err + } + details.buildDefinition = buildDefinition return nil } From 73a9ee36a9ad5e50819ad71e1e412bdf031be013 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 20:17:58 -0700 Subject: [PATCH 14/33] Merging in pipeline detector code. --- cli/azd/cmd/pipeline.go | 2 +- cli/azd/pkg/commands/pipeline/azdo.go | 2 +- cli/azd/pkg/commands/pipeline/pipeline.go | 22 +++++++++++++++---- .../pkg/commands/pipeline/pipeline_test.go | 14 ++++++------ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index a70f45b3fea..00362a7af5f 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -97,7 +97,7 @@ func (p *pipelineConfigAction) Run( // Detect the SCM and CI providers based on the project directory p.manager.ScmProvider, p.manager.CiProvider, - err = pipeline.DetectProviders(ctx, console) + err = pipeline.DetectProviders(ctx, console, env, azdCtx) if err != nil { return err diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 7aba2ff94ff..7b7f41f6b54 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -94,7 +94,7 @@ func getAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName str } } - return nil, fmt.Errorf("error finding default git repository %s in project %s", projectName) + return nil, fmt.Errorf("error finding default git repository in project %s", projectName) } func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 8e32a0f9e12..0ade1a2ce9f 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -101,7 +101,7 @@ func folderExists(folderPath string) bool { // - both .github and .azdo folders found: prompt user to choose provider for scm and ci // - none of the folders found: return error // - no azd context in the ctx: return error -func DetectProviders(ctx context.Context, console input.Console) (ScmProvider, CiProvider, error) { +func DetectProviders(ctx context.Context, console input.Console, env *environment.Environment, azdCtx *azdcontext.AzdContext) (ScmProvider, CiProvider, error) { azdContext, err := azdcontext.GetAzdContext(ctx) if err != nil { return nil, nil, err @@ -123,7 +123,7 @@ func DetectProviders(ctx context.Context, console input.Console) (ScmProvider, C if hasAzDevOpsFolder && !hasGitHubFolder { // Azdo only - return &AzdoHubScmProvider{}, &AzdoCiProvider{}, nil + return createAzdoScmProvider(env, azdContext), createAzdoCiProvider(env, azdContext), nil } // Both folders exist. Prompt to select SCM first @@ -142,7 +142,7 @@ func DetectProviders(ctx context.Context, console input.Console) (ScmProvider, C if scmElection == 1 { // using azdo for scm would only support using azdo pipelines - return &AzdoHubScmProvider{}, &AzdoCiProvider{}, nil + return createAzdoScmProvider(env, azdCtx), createAzdoCiProvider(env, azdContext), nil } // GitHub selected for SCM, prompt for CI provider @@ -164,5 +164,19 @@ func DetectProviders(ctx context.Context, console input.Console) (ScmProvider, C } // GitHub plus azdo pipelines otherwise - return &GitHubScmProvider{}, &AzdoCiProvider{}, nil + return &GitHubScmProvider{}, createAzdoCiProvider(env, azdCtx), nil +} + +func createAzdoCiProvider(env *environment.Environment, azdCtx *azdcontext.AzdContext) *AzdoCiProvider { + return &AzdoCiProvider{ + Env: env, + AzdContext: azdCtx, + } +} + +func createAzdoScmProvider(env *environment.Environment, azdCtx *azdcontext.AzdContext) *AzdoHubScmProvider { + return &AzdoHubScmProvider{ + Env: env, + AzdContext: azdCtx, + } } diff --git a/cli/azd/pkg/commands/pipeline/pipeline_test.go b/cli/azd/pkg/commands/pipeline/pipeline_test.go index 78a1966133a..c6aec61deaa 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_test.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_test.go @@ -22,7 +22,7 @@ func Test_detectProviders(t *testing.T) { ctx := context.Background() t.Run("no azd context within context", func(t *testing.T) { - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) assert.Nil(t, scmProvider) assert.Nil(t, ciProvider) assert.EqualError(t, err, "cannot find AzdContext on go context") @@ -33,7 +33,7 @@ func Test_detectProviders(t *testing.T) { ctx = azdcontext.WithAzdContext(ctx, azdContext) t.Run("no folders error", func(t *testing.T) { - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) assert.Nil(t, scmProvider) assert.Nil(t, ciProvider) assert.EqualError(t, err, "no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory.") @@ -43,7 +43,7 @@ func Test_detectProviders(t *testing.T) { err := os.Mkdir(ghFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &GitHubCiProvider{}, ciProvider) assert.NoError(t, err) @@ -55,7 +55,7 @@ func Test_detectProviders(t *testing.T) { err := os.Mkdir(azdoFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) assert.IsType(t, &AzdoHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) @@ -70,7 +70,7 @@ func Test_detectProviders(t *testing.T) { err = os.Mkdir(ghFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &selectDefaultConsole{}) + scmProvider, ciProvider, err := DetectProviders(ctx, &selectDefaultConsole{}, nil, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &GitHubCiProvider{}, ciProvider) assert.NoError(t, err) @@ -89,7 +89,7 @@ func Test_detectProviders(t *testing.T) { scmProvider, ciProvider, err := DetectProviders(ctx, &circularConsole{ selectReturnValues: []int{1}, - }) + }, nil, nil) assert.IsType(t, &AzdoHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) @@ -108,7 +108,7 @@ func Test_detectProviders(t *testing.T) { scmProvider, ciProvider, err := DetectProviders(ctx, &circularConsole{ selectReturnValues: []int{0, 1}, - }) + }, nil, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) From b0dcd3024c185fc96ce170c64bd770499e538526 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 20:58:40 -0700 Subject: [PATCH 15/33] Added comments to azdo provider functions --- cli/azd/pkg/commands/pipeline/azdo.go | 5 ++-- .../pkg/commands/pipeline/azdo_provider.go | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index 7b7f41f6b54..a613d950fe2 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -35,6 +35,7 @@ var ( AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" CloudEnvironment = "AzureCloud" DefaultBranch = "master" + AzDoProjectDescription = "Azure Dev CLI Project" ) type AzDoClient struct { @@ -63,7 +64,7 @@ func ensureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) } func getAzdoConnection(ctx context.Context, organization string, personalAccessToken string) *azuredevops.Connection { - organizationUrl := fmt.Sprintf("https://dev.azure.com/%s", organization) + organizationUrl := fmt.Sprintf("https://%s/%s", AzDoHostName, organization) connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken) return connection } @@ -240,7 +241,7 @@ func createProject(ctx context.Context, connection *azuredevops.Connection, name func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azuredevops.Connection, env *environment.Environment, console input.Console) (string, string, error) { var project *core.TeamProjectReference currentFolderName := filepath.Base(repoPath) - var projectDescription string = "Azure Dev CLI Project" + var projectDescription string = AzDoProjectDescription for { name, err := console.Prompt(ctx, input.ConsoleOptions{ diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 5ffe7177123..2e15bdc1bb2 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -31,6 +31,9 @@ type AzdoHubScmProvider struct { AzdContext *azdcontext.AzdContext azdoConnection *azuredevops.Connection } + +// AzdoRepositoryDetails provides extra state needed for the AzDo provider. +// this is stored as the details property in repoDetails type AzdoRepositoryDetails struct { projectName string projectId string @@ -64,6 +67,7 @@ func (p *AzdoHubScmProvider) preConfigureCheck(ctx context.Context, console inpu return err } +// helper function to save configuration values to .env file func (p *AzdoHubScmProvider) saveEnvironmentConfig(key string, value string) error { p.Env.Values[key] = value err := p.Env.Save() @@ -79,6 +83,8 @@ func (p *AzdoHubScmProvider) name() string { } // *** scmProvider implementation ****** + +// stores repo details in state for use in other functions. Also saves AzDo project details to .env func (p *AzdoHubScmProvider) StoreRepoDetails(ctx context.Context, repo *azdoGit.GitRepository) error { repoDetails := p.getRepoDetails() repoDetails.repoName = *repo.Name @@ -105,6 +111,7 @@ func (p *AzdoHubScmProvider) StoreRepoDetails(ctx context.Context, repo *azdoGit return nil } +// prompts the user for a new AzDo Git repo and creates the repo func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context, console input.Console) (string, error) { connection, err := p.getAzdoConnection(ctx) if err != nil { @@ -147,6 +154,8 @@ func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context } return *repo.RemoteUrl, nil } + +// verifies that a repo exists or prompts the user to select from a list of existing AzDo repos func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, console input.Console) (string, error) { if p.repoDetails != nil && p.repoDetails.repoName != "" { return p.repoDetails.remoteUrl, nil @@ -184,6 +193,7 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons return updatedRemote, nil } +// helper function to return repoDetails from state func (p *AzdoHubScmProvider) getRepoDetails() *AzdoRepositoryDetails { if p.repoDetails != nil { return p.repoDetails @@ -193,6 +203,7 @@ func (p *AzdoHubScmProvider) getRepoDetails() *AzdoRepositoryDetails { return p.repoDetails } +// helper function to return an azuredevops.Connection for use with AzDo Go SDK func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevops.Connection, error) { if p.azdoConnection != nil { return p.azdoConnection, nil @@ -214,6 +225,8 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop connection := getAzdoConnection(ctx, org, pat) return connection, nil } + +// returns an existing project or prompts the user to either select a project or a create a new AzDo project func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console input.Console) (string, string, bool, error) { if p.repoDetails != nil && p.repoDetails.projectName != "" { return p.repoDetails.projectName, p.repoDetails.projectId, false, nil @@ -294,6 +307,7 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st return remoteUrl, nil } +// returns the git remote for a newly created repo that is part of a newly created AzDo project func (p *AzdoHubScmProvider) getDefaultRepoRemote(ctx context.Context, projectName string, projectId string, console input.Console) (string, error) { connection, err := p.getAzdoConnection(ctx) if err != nil { @@ -315,6 +329,7 @@ func (p *AzdoHubScmProvider) getDefaultRepoRemote(ctx context.Context, projectNa return *repo.RemoteUrl, nil } +// prompt the user to select azdo repo or create a new one func (p *AzdoHubScmProvider) promptForAzdoRepository(ctx context.Context, console input.Console) (string, error) { var remoteUrl string // There are a few ways to configure the remote so offer a choice to the user. @@ -362,6 +377,8 @@ var azdoRemoteHttpsUrlRegex = regexp.MustCompile(`^https://[a-zA-Z0-9]+(?:-[a-zA // ErrRemoteHostIsNotAzDo the error used when a non Azure DevOps remote is found var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an azure devops host") +// helper function to determine if the provided remoteUrl is an azure devops repo. +// currently supports AzDo PaaS func isAzDoRemote(remoteUrl string) error { slug := "" for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} { @@ -415,6 +432,7 @@ func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl strin } // preventGitPush is nil for Azure DevOps +// this method also checks for a credential helper and prompts the user to set a helper to make subsequent pushes easier func (p *AzdoHubScmProvider) preventGitPush( ctx context.Context, gitRepo *gitRepositoryDetails, @@ -459,7 +477,9 @@ func (p *AzdoHubScmProvider) preventGitPush( return false, nil } -// preventGitPush is nil for Azure DevOps +// hook function that fires after a git push +// allows the provider to perform certain tasks after push including +// cleanup on the remote url, creating the build policy for PRs and queuing an initial deployment func (p *AzdoHubScmProvider) postGitPush( ctx context.Context, gitRepo *gitRepositoryDetails, @@ -556,6 +576,7 @@ func (p *AzdoCiProvider) configureConnection( return nil } +// struct used to deserialize service principal json object type AzureServicePrincipalCredentials struct { TenantId string `json:"tenantId"` ClientId string `json:"clientId"` @@ -563,6 +584,7 @@ type AzureServicePrincipalCredentials struct { SubscriptionId string `json:"subscriptionId"` } +// parses the incoming json object and deserializes it to a struct func parseCredentials(ctx context.Context, credentials json.RawMessage) (*AzureServicePrincipalCredentials, error) { azureCredentials := AzureServicePrincipalCredentials{} if e := json.Unmarshal(credentials, &azureCredentials); e != nil { From 69449ee74072a84da07268125c7cfa36bb3fba1e Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 21:14:08 -0700 Subject: [PATCH 16/33] added further comments to azdo.go --- cli/azd/pkg/commands/pipeline/azdo.go | 59 ++++++++++++++++++--------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index a613d950fe2..b6bdd852337 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -22,26 +22,24 @@ import ( ) var ( - AzDoHostName = "dev.azure.com" // hostname of the AzDo PaaS service. - AzDoPatName = "AZURE_DEVOPS_EXT_PAT" // environment variable that holds the Azure DevOps PAT - AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" // environment variable that holds the Azure DevOps Organization Name - AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" - AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" - AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" - AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" - AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" - AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" - AzurePipelineName = "Azure Dev Deploy" - AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" - CloudEnvironment = "AzureCloud" - DefaultBranch = "master" - AzDoProjectDescription = "Azure Dev CLI Project" + AzDoHostName = "dev.azure.com" // hostname of the AzDo PaaS service. + AzDoPatName = "AZURE_DEVOPS_EXT_PAT" // environment variable that holds the Azure DevOps PAT + AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" // environment variable that holds the Azure DevOps Organization Name + AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" // Environment Configuration name used to store the project Id + AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" // Environment Configuration name used to store the project name + AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" // Environment Configuration name used to store repo ID + AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" // Environment Configuration name used to store the Repo Name + AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" // web url for the configured repo. This is displayed on a the command line after a successful invocation of azd pipeline config + AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" // success message after azd pipeline config is successful + AzurePipelineName = "Azure Dev Deploy" // name of the azure pipeline that will be created + AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" // path to the azure pipeline yaml + CloudEnvironment = "AzureCloud" // target Azure Cloud + DefaultBranch = "master" // default branch for pipeline and branch policy + AzDoProjectDescription = "Azure Dev CLI Project" // azure devops project description + ServiceConnectionName = "azconnection" // name of the service connection that will be used in the AzDo project. This will store the Azure service principal ) -type AzDoClient struct { - core.Client -} - +// helper method to verify that a configuration exists in the .env file or in system environment variables func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, key string, label string) (string, error) { value := env.Values[key] if value != "" { @@ -55,20 +53,25 @@ func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, k return value, nil } +// helper method to ensure an Azure DevOps PAT exists either in .env or system environment variables func ensureAzdoPatExists(ctx context.Context, env *environment.Environment) (string, error) { return ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") } +// helper method to ensure an Azure DevOps organization name exists either in .env or system environment variables func ensureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") } +// helper method to return an Azure DevOps connection used the AzDo go sdk func getAzdoConnection(ctx context.Context, organization string, personalAccessToken string) *azuredevops.Connection { organizationUrl := fmt.Sprintf("https://%s/%s", AzDoHostName, organization) connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken) return connection } +// returns a default repo from a newly created AzDo project. +// this relies on the fact that new projects automatically get a repo named the same as the project func getAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName string, connection *azuredevops.Connection) (*git.GitRepository, error) { gitClient, err := git.NewClient(ctx, connection) if err != nil { @@ -98,6 +101,7 @@ func getAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName str return nil, fmt.Errorf("error finding default git repository in project %s", projectName) } +// prompt the user to select a repo and return a repository object func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { gitClient, err := git.NewClient(ctx, connection) if err != nil { @@ -140,6 +144,7 @@ func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, or return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) } +// create a new repository in the current project func createRepository(ctx context.Context, projectId string, repoName string, connection *azuredevops.Connection) (*git.GitRepository, error) { gitClient, err := git.NewClient(ctx, connection) if err != nil { @@ -161,6 +166,7 @@ func createRepository(ctx context.Context, projectId string, repoName string, co return repo, nil } +// returns a process template (basic, agile etc) used in the new project creation flow func getProcessTemplateId(ctx context.Context, client core.Client) (string, error) { processArgs := core.GetProcessesArgs{} processes, err := client.GetProcesses(ctx, processArgs) @@ -171,6 +177,7 @@ func getProcessTemplateId(ctx context.Context, client core.Client) (string, erro return fmt.Sprintf("%s", process.Id), nil } +// creates a new Azure Devops project func createProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { @@ -238,6 +245,8 @@ func createProject(ctx context.Context, connection *azuredevops.Connection, name return project, nil } +// prompts the user for a new AzDo project name and creates the project +// returns project name, project id, error func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azuredevops.Connection, env *environment.Environment, console input.Console) (string, string, error) { var project *core.TeamProjectReference currentFolderName := filepath.Base(repoPath) @@ -273,6 +282,7 @@ func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu return *project.Name, project.Id.String(), nil } +// return an azdo project by name func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connection, name string) (*core.TeamProjectReference, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { @@ -295,6 +305,7 @@ func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connectio return nil, fmt.Errorf("azure devops project %s not found", name) } +// prompt the user to select form a list of existing Azure DevOps projects func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, string, error) { coreClient, err := core.NewClient(ctx, connection) if err != nil { @@ -327,6 +338,7 @@ func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Con return options[projectIdx], projectsList[projectIdx].Id.String(), nil } +// Creates a variable to be associated with a Pipeline func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bool) build.BuildDefinitionVariable { return build.BuildDefinitionVariable{ AllowOverride: &allowOverride, @@ -335,6 +347,7 @@ func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bo } } +// returns the default agent queue. This is used to associate a Pipeline with a default agent pool queue func getAgentQueue(ctx context.Context, projectId string, connection *azuredevops.Connection) (*taskagent.TaskAgentQueue, error) { client, err := taskagent.NewClient(ctx, connection) if err != nil { @@ -355,6 +368,7 @@ func getAgentQueue(ctx context.Context, projectId string, connection *azuredevop return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) } +// create a new Azure DevOps pipeline func createPipeline( ctx context.Context, projectId string, @@ -438,6 +452,7 @@ func createPipeline( return newBuildDefinition, nil } +// run a pipeline. This is used to invoke the deploy pipeline after a successful push of the code func queueBuild( ctx context.Context, connection *azuredevops.Connection, @@ -468,6 +483,8 @@ func queueBuild( return nil } + +// authorize a service connection to be used in all pipelines func authorizeServiceConnectionToAllPipelines( ctx context.Context, projectId string, @@ -501,6 +518,7 @@ func authorizeServiceConnectionToAllPipelines( return nil } +// create a new service connection that will be used in the deployment pipeline func createServiceConnection( ctx context.Context, connection *azuredevops.Connection, @@ -518,7 +536,7 @@ func createServiceConnection( endpointType := "azurerm" endpointOwner := "library" endpointUrl := "https://management.azure.com/" - endpointName := "azconnection" + endpointName := ServiceConnectionName endpointIsShared := false endpointScheme := "ServicePrincipal" @@ -563,6 +581,7 @@ func createServiceConnection( return nil } +// returns a build policy type named "Build." Used to created the PR build policy on the default branch func getBuildType(ctx context.Context, projectId *string, policyClient policy.Client) (*policy.PolicyType, error) { getPolicyTypesArgs := policy.GetPolicyTypesArgs{ Project: projectId, @@ -579,6 +598,8 @@ func getBuildType(ctx context.Context, projectId *string, policyClient policy.Cl return nil, fmt.Errorf("could not find 'Build' policy type in project") } +// create the PR build policy to ensure that the pipeline runs on a new pull request +// this also disables direct pushes to the default branch and requires changes to go through a PR. func createBuildPolicy( ctx context.Context, connection *azuredevops.Connection, From f2324a96403bac331367262e41552c880a93ab5a Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Fri, 16 Sep 2022 21:29:09 -0700 Subject: [PATCH 17/33] Removing azdcontext from DetectProviders --- cli/azd/cmd/pipeline.go | 2 +- cli/azd/pkg/commands/pipeline/pipeline.go | 6 +++--- cli/azd/pkg/commands/pipeline/pipeline_test.go | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 00362a7af5f..2259c962bdc 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -97,7 +97,7 @@ func (p *pipelineConfigAction) Run( // Detect the SCM and CI providers based on the project directory p.manager.ScmProvider, p.manager.CiProvider, - err = pipeline.DetectProviders(ctx, console, env, azdCtx) + err = pipeline.DetectProviders(ctx, console, env) if err != nil { return err diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 0ade1a2ce9f..278a6fb710c 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -101,7 +101,7 @@ func folderExists(folderPath string) bool { // - both .github and .azdo folders found: prompt user to choose provider for scm and ci // - none of the folders found: return error // - no azd context in the ctx: return error -func DetectProviders(ctx context.Context, console input.Console, env *environment.Environment, azdCtx *azdcontext.AzdContext) (ScmProvider, CiProvider, error) { +func DetectProviders(ctx context.Context, console input.Console, env *environment.Environment) (ScmProvider, CiProvider, error) { azdContext, err := azdcontext.GetAzdContext(ctx) if err != nil { return nil, nil, err @@ -142,7 +142,7 @@ func DetectProviders(ctx context.Context, console input.Console, env *environmen if scmElection == 1 { // using azdo for scm would only support using azdo pipelines - return createAzdoScmProvider(env, azdCtx), createAzdoCiProvider(env, azdContext), nil + return createAzdoScmProvider(env, azdContext), createAzdoCiProvider(env, azdContext), nil } // GitHub selected for SCM, prompt for CI provider @@ -164,7 +164,7 @@ func DetectProviders(ctx context.Context, console input.Console, env *environmen } // GitHub plus azdo pipelines otherwise - return &GitHubScmProvider{}, createAzdoCiProvider(env, azdCtx), nil + return &GitHubScmProvider{}, createAzdoCiProvider(env, azdContext), nil } func createAzdoCiProvider(env *environment.Environment, azdCtx *azdcontext.AzdContext) *AzdoCiProvider { diff --git a/cli/azd/pkg/commands/pipeline/pipeline_test.go b/cli/azd/pkg/commands/pipeline/pipeline_test.go index c6aec61deaa..34b37d3a6fe 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_test.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_test.go @@ -22,7 +22,7 @@ func Test_detectProviders(t *testing.T) { ctx := context.Background() t.Run("no azd context within context", func(t *testing.T) { - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil) assert.Nil(t, scmProvider) assert.Nil(t, ciProvider) assert.EqualError(t, err, "cannot find AzdContext on go context") @@ -33,7 +33,7 @@ func Test_detectProviders(t *testing.T) { ctx = azdcontext.WithAzdContext(ctx, azdContext) t.Run("no folders error", func(t *testing.T) { - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil) assert.Nil(t, scmProvider) assert.Nil(t, ciProvider) assert.EqualError(t, err, "no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory.") @@ -43,7 +43,7 @@ func Test_detectProviders(t *testing.T) { err := os.Mkdir(ghFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &GitHubCiProvider{}, ciProvider) assert.NoError(t, err) @@ -55,7 +55,7 @@ func Test_detectProviders(t *testing.T) { err := os.Mkdir(azdoFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil, nil) + scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil) assert.IsType(t, &AzdoHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) @@ -70,7 +70,7 @@ func Test_detectProviders(t *testing.T) { err = os.Mkdir(ghFolder, osutil.PermissionDirectory) assert.NoError(t, err) - scmProvider, ciProvider, err := DetectProviders(ctx, &selectDefaultConsole{}, nil, nil) + scmProvider, ciProvider, err := DetectProviders(ctx, &selectDefaultConsole{}, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &GitHubCiProvider{}, ciProvider) assert.NoError(t, err) @@ -89,7 +89,7 @@ func Test_detectProviders(t *testing.T) { scmProvider, ciProvider, err := DetectProviders(ctx, &circularConsole{ selectReturnValues: []int{1}, - }, nil, nil) + }, nil) assert.IsType(t, &AzdoHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) @@ -108,7 +108,7 @@ func Test_detectProviders(t *testing.T) { scmProvider, ciProvider, err := DetectProviders(ctx, &circularConsole{ selectReturnValues: []int{0, 1}, - }, nil, nil) + }, nil) assert.IsType(t, &GitHubScmProvider{}, scmProvider) assert.IsType(t, &AzdoCiProvider{}, ciProvider) assert.NoError(t, err) From d1b2a5b9bea43510ae8981c7e970b27b0a862507 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:21:54 -0700 Subject: [PATCH 18/33] Adding azdo provider api literal property names to cpell global list. --- .vscode/cspell.global.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index ed42ab95cc7..636d27681ad 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -96,6 +96,14 @@ ignoreWords: - menuid - PLACEHOLDERIACTOOLS - Azdo + - azconnection + - azuredevops + - taskagent + - tfsgit + - serviceendpoint + - serviceprincipalid + - serviceprincipalkey + - tenantid useGitignore: true dictionaryDefinitions: - name: gitHubUserAliases From 77dcf1f92d9fc4a65a9e4b5896248f7a3d8ef8e0 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:31:38 -0700 Subject: [PATCH 19/33] Adding missing error checks. --- cli/azd/pkg/commands/pipeline/azdo.go | 6 ++++- .../pkg/commands/pipeline/azdo_provider.go | 23 +++++++++++-------- .../pkg/commands/pipeline/github_provider.go | 4 ++-- cli/azd/pkg/commands/pipeline/pipeline.go | 2 +- .../pkg/commands/pipeline/pipeline_manager.go | 4 ++-- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index b6bdd852337..fe028a58cb2 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -653,7 +653,11 @@ func createBuildPolicy( Project: &projectId, Configuration: policyConfiguration, } - client.CreatePolicyConfiguration(ctx, createPolicyConfigurationArgs) + + _, err = client.CreatePolicyConfiguration(ctx, createPolicyConfigurationArgs) + if err != nil { + return err + } return nil } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 2e15bdc1bb2..93995fcfd0a 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -460,12 +460,15 @@ func (p *AzdoHubScmProvider) preventGitPush( DefaultValue: "Yes - set credential.helper = store", }) if err != nil { - return false, fmt.Errorf("prompting for credential helper: %w", err) + return false, fmt.Errorf("prompting for credential helper: %w ", err) } switch idx { // Configure Credential Store case 0: - gitCli.SetCredentialStore(ctx, p.AzdContext.ProjectDirectory()) + err = gitCli.SetCredentialStore(ctx, p.AzdContext.ProjectDirectory()) + if err != nil { + return false, fmt.Errorf("storing credentials in env: %w ", err) + } // Skip case 1: break @@ -485,33 +488,35 @@ func (p *AzdoHubScmProvider) postGitPush( gitRepo *gitRepositoryDetails, remoteName string, branchName string, - console input.Console) (bool, error) { + console input.Console) error { gitCli := git.NewGitCli(ctx) //Reset remote to original url without PAT - gitCli.UpdateRemote(ctx, p.AzdContext.ProjectDirectory(), remoteName, p.repoDetails.remoteUrl) - + err := gitCli.UpdateRemote(ctx, p.AzdContext.ProjectDirectory(), remoteName, p.repoDetails.remoteUrl) + if err != nil { + return err + } if gitRepo.pushStatus { console.Message(ctx, output.WithSuccessFormat(AzdoConfigSuccessMessage, p.repoDetails.repoWebUrl)) } connection, err := p.getAzdoConnection(ctx) if err != nil { - return false, err + return err } err = createBuildPolicy(ctx, connection, p.repoDetails.projectId, p.repoDetails.repoId, p.repoDetails.buildDefinition) if err != nil { - return false, err + return err } err = queueBuild(ctx, connection, p.repoDetails.projectId, p.repoDetails.buildDefinition) if err != nil { - return false, err + return err } - return false, nil + return nil } // AzdoCiProvider implements a CiProvider using Azure DevOps to manage CI with azdo pipelines. diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index c111edcba67..bf41fc69abd 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -155,8 +155,8 @@ func (p *GitHubScmProvider) postGitPush( gitRepo *gitRepositoryDetails, remoteName string, branchName string, - console input.Console) (bool, error) { - return false, nil + console input.Console) error { + return nil } // enum type for taking a choice after finding GitHub actions disabled. diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 278a6fb710c..c0c3d058109 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -67,7 +67,7 @@ type ScmProvider interface { gitRepo *gitRepositoryDetails, remoteName string, branchName string, - console input.Console) (bool, error) + console input.Console) error } // CiProvider defines the base behavior for a continuous integration provider. diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 991b8498c7b..117d6711d4c 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -292,14 +292,14 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { if err == nil { gitRepoInfo.pushStatus = true } - manager.ScmProvider.postGitPush( + err = manager.ScmProvider.postGitPush( ctx, gitRepoInfo, manager.PipelineRemoteName, currentBranch, inputConsole) if err != nil { - return err + return fmt.Errorf(" post git push hook : ") } } else { inputConsole.Message(ctx, From ef8c3778d0ac1e45c5074c2d57d07718509e7aaa Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:32:15 -0700 Subject: [PATCH 20/33] Removing unused variable --- cli/azd/pkg/commands/pipeline/azdo_provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 93995fcfd0a..f06b3419bee 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -43,7 +43,6 @@ type AzdoRepositoryDetails struct { repoWebUrl string remoteUrl string sshUrl string - pushSuccess bool buildDefinition *build.BuildDefinition } From 7e0faac812bd942501784268f2770a3642aff4e6 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:43:12 -0700 Subject: [PATCH 21/33] Fixing golint error --- cli/azd/pkg/commands/pipeline/azdo.go | 8 +++++--- cli/azd/pkg/commands/pipeline/azdo_provider.go | 5 ++++- cli/azd/pkg/commands/pipeline/github_provider.go | 1 - cli/azd/pkg/commands/pipeline/pipeline.go | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go index fe028a58cb2..4db553acbe4 100644 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ b/cli/azd/pkg/commands/pipeline/azdo.go @@ -174,7 +174,7 @@ func getProcessTemplateId(ctx context.Context, client core.Client) (string, erro return "", err } process := (*processes)[0] - return fmt.Sprintf("%s", process.Id), nil + return process.Id.String(), nil } // creates a new Azure Devops project @@ -576,8 +576,10 @@ func createServiceConnection( return err } - authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) - + err = authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) + if err != nil { + return err + } return nil } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index f06b3419bee..7e5a1752b75 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -576,7 +576,10 @@ func (p *AzdoCiProvider) configureConnection( } connection := getAzdoConnection(ctx, org, pat) - createServiceConnection(ctx, connection, details.projectId, *p.Env, repoDetails, *p.credentials, console) + err = createServiceConnection(ctx, connection, details.projectId, *p.Env, repoDetails, *p.credentials, console) + if err != nil { + return err + } return nil } diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index bf41fc69abd..3adb89785b7 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -27,7 +27,6 @@ import ( // for source control manager. type GitHubScmProvider struct { newGitHubRepoCreated bool - environment *environment.Environment } // *** subareaProvider implementation ****** diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index c0c3d058109..235cc27a1eb 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -113,7 +113,7 @@ func DetectProviders(ctx context.Context, console input.Console, env *environmen hasAzDevOpsFolder := folderExists(path.Join(projectDir, ".azdo")) if !hasGitHubFolder && !hasAzDevOpsFolder { - return nil, nil, fmt.Errorf("no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory.") + return nil, nil, fmt.Errorf("no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory") } if !hasAzDevOpsFolder && hasGitHubFolder { From 92addbf0e920a7eae905e772325d0b628a490bd1 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:46:59 -0700 Subject: [PATCH 22/33] Removing unused development test. --- cli/azd/test/functional/cli_test.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/cli/azd/test/functional/cli_test.go b/cli/azd/test/functional/cli_test.go index 527880adc2f..60ee3f6e4f3 100644 --- a/cli/azd/test/functional/cli_test.go +++ b/cli/azd/test/functional/cli_test.go @@ -815,27 +815,6 @@ func Test_CLI_InfraCreateAndDeleteResourceTerraformRemote(t *testing.T) { t.Logf("Done\n") } -func Test_CLI_PipelineConfig(t *testing.T) { - // running this test in parallel is ok as it uses a t.TempDir() - t.Parallel() - - dir := t.TempDir() - t.Logf("DIR: %s", dir) - - envName := randomEnvName() - t.Logf("AZURE_ENV_NAME: %s", envName) - - cli := azdcli.NewCLI(t) - cli.WorkingDirectory = dir - cli.Env = append(os.Environ(), "AZURE_LOCATION=eastus2") - - t.Logf("Starting pipeline config\n") - err := cmd.Execute([]string{"pipeline", "config", "--cwd", dir, "--principal-name", "mock-principal"}) - require.NoError(t, err) - - t.Logf("Done\n") -} - func TestMain(m *testing.M) { flag.Parse() shortFlag := flag.Lookup("test.short") From e484a5d211dcc277dc68f090fe5dec8b6464c005 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 08:49:24 -0700 Subject: [PATCH 23/33] Adding versioncontrol string literal to cspell. Used by the azdo api. --- .vscode/cspell.global.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 636d27681ad..fe63463ea95 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -104,6 +104,7 @@ ignoreWords: - serviceprincipalid - serviceprincipalkey - tenantid + - versioncontrol useGitignore: true dictionaryDefinitions: - name: gitHubUserAliases From 375240769549b15e3b8899a1b8590215d3f89000 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sat, 17 Sep 2022 09:05:39 -0700 Subject: [PATCH 24/33] Modifying pipeline test check. Punctuation removed from error in code and test based on linting results. --- cli/azd/pkg/commands/pipeline/pipeline_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/commands/pipeline/pipeline_test.go b/cli/azd/pkg/commands/pipeline/pipeline_test.go index 34b37d3a6fe..5503650a190 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_test.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_test.go @@ -36,7 +36,7 @@ func Test_detectProviders(t *testing.T) { scmProvider, ciProvider, err := DetectProviders(ctx, &nullConsole{}, nil) assert.Nil(t, scmProvider) assert.Nil(t, ciProvider) - assert.EqualError(t, err, "no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory.") + assert.EqualError(t, err, "no CI/CD provider configuration found. Expecting either .github and/or .azdo folder in the project root directory") }) t.Run("github folder only", func(t *testing.T) { ghFolder := path.Join(tempDir, ".github") From 93a099ec496e872681e194bcc39eedd13ebc8566 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Sun, 18 Sep 2022 23:52:18 -0700 Subject: [PATCH 25/33] Azdo provider refactor (#45) * Refactor to introduce new azdo package * refactor azdo code to multiple files in azdo package * Added azdo connection test and refactored provider * Added azdo provider initial tests * added more azdo provider tests * Adding test to verify save env config in azdo provider * Updating displayed urls to aka.ms --- cli/azd/pkg/azdo/azdo.go | 49 ++ cli/azd/pkg/azdo/azdo_test.go | 26 + cli/azd/pkg/azdo/build_policy.go | 91 +++ cli/azd/pkg/azdo/pipeline.go | 157 +++++ cli/azd/pkg/azdo/project.go | 187 +++++ cli/azd/pkg/azdo/repository.go | 106 +++ cli/azd/pkg/azdo/service_connection.go | 109 +++ cli/azd/pkg/azdo/utils.go | 33 + cli/azd/pkg/commands/pipeline/azdo.go | 665 ------------------ .../pkg/commands/pipeline/azdo_provider.go | 99 +-- .../commands/pipeline/azdo_provider_test.go | 158 +++++ .../commands/pipeline/github_provider_test.go | 2 +- 12 files changed, 968 insertions(+), 714 deletions(-) create mode 100644 cli/azd/pkg/azdo/azdo.go create mode 100644 cli/azd/pkg/azdo/azdo_test.go create mode 100644 cli/azd/pkg/azdo/build_policy.go create mode 100644 cli/azd/pkg/azdo/pipeline.go create mode 100644 cli/azd/pkg/azdo/project.go create mode 100644 cli/azd/pkg/azdo/repository.go create mode 100644 cli/azd/pkg/azdo/service_connection.go create mode 100644 cli/azd/pkg/azdo/utils.go delete mode 100644 cli/azd/pkg/commands/pipeline/azdo.go create mode 100644 cli/azd/pkg/commands/pipeline/azdo_provider_test.go diff --git a/cli/azd/pkg/azdo/azdo.go b/cli/azd/pkg/azdo/azdo.go new file mode 100644 index 00000000000..7857b7f100e --- /dev/null +++ b/cli/azd/pkg/azdo/azdo.go @@ -0,0 +1,49 @@ +package azdo + +import ( + "context" + "fmt" + + "github.com/microsoft/azure-devops-go-api/azuredevops" +) + +var ( + AzDoHostName = "dev.azure.com" // hostname of the AzDo PaaS service. + AzDoPatName = "AZURE_DEVOPS_EXT_PAT" // environment variable that holds the Azure DevOps PAT + AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" // environment variable that holds the Azure DevOps Organization Name + AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" // Environment Configuration name used to store the project Id + AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" // Environment Configuration name used to store the project name + AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" // Environment Configuration name used to store repo ID + AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" // Environment Configuration name used to store the Repo Name + AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" // web url for the configured repo. This is displayed on a the command line after a successful invocation of azd pipeline config + AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" // success message after azd pipeline config is successful + AzurePipelineName = "Azure Dev Deploy" // name of the azure pipeline that will be created + AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" // path to the azure pipeline yaml + CloudEnvironment = "AzureCloud" // target Azure Cloud + DefaultBranch = "master" // default branch for pipeline and branch policy + AzDoProjectDescription = "Azure Dev CLI Project" // azure devops project description + ServiceConnectionName = "azconnection" // name of the service connection that will be used in the AzDo project. This will store the Azure service principal +) + +type AzureServicePrincipalCredentials struct { + TenantId string `json:"tenantId"` + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + SubscriptionId string `json:"subscriptionId"` +} + +// helper method to return an Azure DevOps connection used the AzDo go sdk +func GetAzdoConnection(ctx context.Context, organization string, personalAccessToken string) (*azuredevops.Connection, error) { + if organization == "" { + return nil, fmt.Errorf("organization name is required") + } + + if personalAccessToken == "" { + return nil, fmt.Errorf("personal access token is required") + } + + organizationUrl := fmt.Sprintf("https://%s/%s", AzDoHostName, organization) + connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken) + + return connection, nil +} diff --git a/cli/azd/pkg/azdo/azdo_test.go b/cli/azd/pkg/azdo/azdo_test.go new file mode 100644 index 00000000000..ef77a074e38 --- /dev/null +++ b/cli/azd/pkg/azdo/azdo_test.go @@ -0,0 +1,26 @@ +package azdo + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getAzdoConnection(t *testing.T) { + ctx := context.Background() + t.Run("empty organization name error", func(t *testing.T) { + _, err := GetAzdoConnection(ctx, "", "") + assert.EqualError(t, err, "organization name is required") + }) + + t.Run("empty pat error", func(t *testing.T) { + _, err := GetAzdoConnection(ctx, "fake_org", "") + assert.EqualError(t, err, "personal access token is required") + }) + t.Run("returns a connection", func(t *testing.T) { + connection, err := GetAzdoConnection(ctx, "fake_org", "fake_pat") + assert.Nil(t, err) + assert.NotNil(t, connection) + }) +} diff --git a/cli/azd/pkg/azdo/build_policy.go b/cli/azd/pkg/azdo/build_policy.go new file mode 100644 index 00000000000..2ce1cdd7aad --- /dev/null +++ b/cli/azd/pkg/azdo/build_policy.go @@ -0,0 +1,91 @@ +package azdo + +import ( + "context" + "fmt" + + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/build" + "github.com/microsoft/azure-devops-go-api/azuredevops/policy" +) + +// returns a build policy type named "Build." Used to created the PR build policy on the default branch +func getBuildType(ctx context.Context, projectId *string, policyClient policy.Client) (*policy.PolicyType, error) { + getPolicyTypesArgs := policy.GetPolicyTypesArgs{ + Project: projectId, + } + policyTypes, err := policyClient.GetPolicyTypes(ctx, getPolicyTypesArgs) + if err != nil { + return nil, err + } + for _, policy := range *policyTypes { + if *policy.DisplayName == "Build" { + return &policy, nil + } + } + return nil, fmt.Errorf("could not find 'Build' policy type in project") +} + +// create the PR build policy to ensure that the pipeline runs on a new pull request +// this also disables direct pushes to the default branch and requires changes to go through a PR. +func CreateBuildPolicy( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + repoId string, + buildDefinition *build.BuildDefinition) error { + client, err := policy.NewClient(ctx, connection) + if err != nil { + return err + } + + buildPolicyType, err := getBuildType(ctx, &projectId, client) + if err != nil { + return err + } + + policyTypeRef := &policy.PolicyTypeRef{ + Id: buildPolicyType.Id, + } + policyRevision := 1 + policyIsDeleted := false + policyIsBlocking := true + policyIsEnabled := true + + policySettingsScope := make(map[string]interface{}) + policySettingsScope["repositoryId"] = repoId + policySettingsScope["refName"] = fmt.Sprintf("refs/heads/%s", DefaultBranch) + policySettingsScope["matchKind"] = "Exact" + + policySettingsScopes := make([]map[string]interface{}, 1) + policySettingsScopes[0] = policySettingsScope + + policySettings := make(map[string]interface{}) + policySettings["buildDefinitionId"] = buildDefinition.Id + policySettings["displayName"] = "Azure Dev Deploy PR" + policySettings["manualQueueOnly"] = false + policySettings["queueOnSourceUpdateOnly"] = true + policySettings["validDuration"] = 720 + policySettings["scope"] = policySettingsScopes + + policyConfiguration := &policy.PolicyConfiguration{ + Type: policyTypeRef, + Revision: &policyRevision, + IsDeleted: &policyIsDeleted, + IsBlocking: &policyIsBlocking, + IsEnabled: &policyIsEnabled, + Settings: policySettings, + } + + createPolicyConfigurationArgs := policy.CreatePolicyConfigurationArgs{ + Project: &projectId, + Configuration: policyConfiguration, + } + + _, err = client.CreatePolicyConfiguration(ctx, createPolicyConfigurationArgs) + if err != nil { + return err + } + + return nil +} diff --git a/cli/azd/pkg/azdo/pipeline.go b/cli/azd/pkg/azdo/pipeline.go new file mode 100644 index 00000000000..4ab8d0ea2a8 --- /dev/null +++ b/cli/azd/pkg/azdo/pipeline.go @@ -0,0 +1,157 @@ +package azdo + +import ( + "context" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/build" + "github.com/microsoft/azure-devops-go-api/azuredevops/taskagent" +) + +// Creates a variable to be associated with a Pipeline +func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bool) build.BuildDefinitionVariable { + return build.BuildDefinitionVariable{ + AllowOverride: &allowOverride, + IsSecret: &isSecret, + Value: &value, + } +} + +// returns the default agent queue. This is used to associate a Pipeline with a default agent pool queue +func getAgentQueue(ctx context.Context, projectId string, connection *azuredevops.Connection) (*taskagent.TaskAgentQueue, error) { + client, err := taskagent.NewClient(ctx, connection) + if err != nil { + return nil, err + } + getAgentQueuesArgs := taskagent.GetAgentQueuesArgs{ + Project: &projectId, + } + queues, err := client.GetAgentQueues(ctx, getAgentQueuesArgs) + if err != nil { + return nil, err + } + for _, queue := range *queues { + if *queue.Name == "Default" { + return &queue, nil + } + } + return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) +} + +// create a new Azure DevOps pipeline +func CreatePipeline( + ctx context.Context, + projectId string, + name string, + repoName string, + connection *azuredevops.Connection, + credentials AzureServicePrincipalCredentials, + env environment.Environment) (*build.BuildDefinition, error) { + + client, err := build.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + repoType := "tfsgit" + buildDefinitionType := build.DefinitionType("build") + definitionQueueStatus := build.DefinitionQueueStatus("enabled") + defaultBranch := fmt.Sprintf("refs/heads/%s", DefaultBranch) + buildRepository := &build.BuildRepository{ + Type: &repoType, + Name: &repoName, + DefaultBranch: &defaultBranch, + } + + process := make(map[string]interface{}) + process["type"] = 2 + process["yamlFilename"] = AzurePipelineYamlPath + + variables := make(map[string]build.BuildDefinitionVariable) + variables["AZURE_SUBSCRIPTION_ID"] = createBuildDefinitionVariable(credentials.SubscriptionId, false, false) + variables["ARM_TENANT_ID"] = createBuildDefinitionVariable(credentials.TenantId, false, false) + variables["ARM_CLIENT_ID"] = createBuildDefinitionVariable(credentials.ClientId, true, false) + variables["ARM_CLIENT_SECRET"] = createBuildDefinitionVariable(credentials.ClientSecret, true, false) + variables["AZURE_LOCATION"] = createBuildDefinitionVariable(env.GetLocation(), false, false) + variables["AZURE_ENV_NAME"] = createBuildDefinitionVariable(env.GetEnvName(), false, false) + + queue, err := getAgentQueue(ctx, projectId, connection) + if err != nil { + return nil, err + } + + agentPoolQueue := &build.AgentPoolQueue{ + Id: queue.Id, + Name: queue.Name, + } + + trigger := make(map[string]interface{}) + trigger["batchChanges"] = false + trigger["maxConcurrentBuildsPerBranch"] = 1 + trigger["pollingInterval"] = 0 + trigger["isSettingsSourceOptionSupported"] = true + trigger["defaultSettingsSourceType"] = 2 + trigger["settingsSourceType"] = 2 + trigger["defaultSettingsSourceType"] = 2 + trigger["triggerType"] = 2 + + triggers := make([]interface{}, 1) + triggers[0] = trigger + + buildDefinition := &build.BuildDefinition{ + Name: &name, + Type: &buildDefinitionType, + QueueStatus: &definitionQueueStatus, + Repository: buildRepository, + Process: process, + Queue: agentPoolQueue, + Variables: &variables, + Triggers: &triggers, + } + + createDefinitionArgs := &build.CreateDefinitionArgs{ + Project: &projectId, + Definition: buildDefinition, + } + + newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) + if err != nil { + return nil, err + } + + return newBuildDefinition, nil +} + +// run a pipeline. This is used to invoke the deploy pipeline after a successful push of the code +func QueueBuild( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + buildDefinition *build.BuildDefinition) error { + client, err := build.NewClient(ctx, connection) + if err != nil { + return err + } + definitionReference := &build.DefinitionReference{ + Id: buildDefinition.Id, + } + + newBuild := &build.Build{ + Definition: definitionReference, + } + queueBuildArgs := build.QueueBuildArgs{ + Project: &projectId, + Build: newBuild, + } + + //time.Sleep(500 * time.Millisecond) + + _, err = client.QueueBuild(ctx, queueBuildArgs) + if err != nil { + return err + } + + return nil +} diff --git a/cli/azd/pkg/azdo/project.go b/cli/azd/pkg/azdo/project.go new file mode 100644 index 00000000000..7ac6b2f2695 --- /dev/null +++ b/cli/azd/pkg/azdo/project.go @@ -0,0 +1,187 @@ +package azdo + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/core" + "github.com/microsoft/azure-devops-go-api/azuredevops/operations" +) + +// returns a process template (basic, agile etc) used in the new project creation flow +func getProcessTemplateId(ctx context.Context, client core.Client) (string, error) { + processArgs := core.GetProcessesArgs{} + processes, err := client.GetProcesses(ctx, processArgs) + if err != nil { + return "", err + } + process := (*processes)[0] + return process.Id.String(), nil +} + +// creates a new Azure Devops project +func createProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + processTemplateId, err := getProcessTemplateId(ctx, coreClient) + if err != nil { + return nil, fmt.Errorf("error fetching process template id %w", err) + } + + capabilities := map[string]map[string]string{ + "versioncontrol": { + "sourceControlType": "git", + }, + "processTemplate": { + "templateTypeId": processTemplateId, + }, + } + args := core.QueueCreateProjectArgs{ + ProjectToCreate: &core.TeamProject{ + Description: &description, + Name: &name, + Visibility: &core.ProjectVisibilityValues.Private, + Capabilities: &capabilities, + }, + } + res, err := coreClient.QueueCreateProject(ctx, args) + if err != nil { + return nil, err + } + + operationsClient := operations.NewClient(ctx, connection) + + getOperationsArgs := operations.GetOperationArgs{ + OperationId: res.Id, + } + + projectCreated := false + maxCheck := 10 + count := 0 + + for !projectCreated { + operation, err := operationsClient.GetOperation(ctx, getOperationsArgs) + if err != nil { + return nil, err + } + + if *operation.Status == "succeeded" { + projectCreated = true + } + + if count >= maxCheck { + return nil, fmt.Errorf("error creating azure devops project %s", name) + } + + count++ + time.Sleep(700 * time.Millisecond) + } + + project, err := getAzdoProjectByName(ctx, connection, name) + if err != nil { + return nil, err + } + return project, nil +} + +// prompts the user for a new AzDo project name and creates the project +// returns project name, project id, error +func GetAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azuredevops.Connection, env *environment.Environment, console input.Console) (string, string, error) { + var project *core.TeamProjectReference + currentFolderName := filepath.Base(repoPath) + var projectDescription string = AzDoProjectDescription + + for { + name, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Enter the name for your new Azure Devops Project OR Hit enter to use this name:", + DefaultValue: currentFolderName, + }) + if err != nil { + return "", "", fmt.Errorf("asking for new project name: %w", err) + } + var message string = "" + newProject, err := createProject(ctx, connection, name, projectDescription, console) + if err != nil { + message = err.Error() + } + if strings.Contains(message, fmt.Sprintf("The following project already exists on the Azure DevOps Server: %s", name)) { + console.Message(ctx, fmt.Sprintf("error: the project name '%s' is already in use\n", name)) + continue // try again + } else if strings.Contains(message, "The following name is not valid") { + console.Message(ctx, fmt.Sprintf("error: the project name '%s' is not a valid Azure DevOps project Name. See https://aka.ms/azure-dev/azdo-project-naming\n", name)) + continue // try again + } else if err != nil { + return "", "", fmt.Errorf("creating project: %w", err) + } else { + project = newProject + break + } + } + + return *project.Name, project.Id.String(), nil +} + +// return an azdo project by name +func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connection, name string) (*core.TeamProjectReference, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + args := core.GetProjectsArgs{} + getProjectsResponse, err := coreClient.GetProjects(ctx, args) + if err != nil { + return nil, err + } + + projects := getProjectsResponse.Value + for _, project := range projects { + if *project.Name == name { + return &project, nil + } + } + + return nil, fmt.Errorf("azure devops project %s not found", name) +} + +// prompt the user to select form a list of existing Azure DevOps projects +func GetAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, string, error) { + coreClient, err := core.NewClient(ctx, connection) + if err != nil { + return "", "", err + } + + args := core.GetProjectsArgs{} + getProjectsResponse, err := coreClient.GetProjects(ctx, args) + if err != nil { + return "", "", err + } + + projects := getProjectsResponse.Value + projectsList := make([]core.TeamProjectReference, len(projects)) + options := make([]string, len(projects)) + for idx, project := range projects { + options[idx] = *project.Name + projectsList[idx] = project + } + + projectIdx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Please choose an existing Azure DevOps Project", + Options: options, + }) + + if err != nil { + return "", "", fmt.Errorf("prompting for azdo project: %w", err) + } + + return options[projectIdx], projectsList[projectIdx].Id.String(), nil +} diff --git a/cli/azd/pkg/azdo/repository.go b/cli/azd/pkg/azdo/repository.go new file mode 100644 index 00000000000..28b6ec4bd61 --- /dev/null +++ b/cli/azd/pkg/azdo/repository.go @@ -0,0 +1,106 @@ +package azdo + +import ( + "context" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/git" +) + +// create a new repository in the current project +func CreateRepository(ctx context.Context, projectId string, repoName string, connection *azuredevops.Connection) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + gitRepositoryCreateOptions := git.GitRepositoryCreateOptions{ + Name: &repoName, + } + + createRepositoryArgs := git.CreateRepositoryArgs{ + Project: &projectId, + GitRepositoryToCreate: &gitRepositoryCreateOptions, + } + repo, err := gitClient.CreateRepository(ctx, createRepositoryArgs) + if err != nil { + return nil, err + } + return repo, nil +} + +// returns a default repo from a newly created AzDo project. +// this relies on the fact that new projects automatically get a repo named the same as the project +func GetAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName string, connection *azuredevops.Connection) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + includeLinks := true + includeAllUrls := true + repoArgs := git.GetRepositoriesArgs{ + Project: &projectName, + IncludeLinks: &includeLinks, + IncludeAllUrls: &includeAllUrls, + } + + getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) + if err != nil { + return nil, err + } + repos := *getRepositoriesResult + + for _, repo := range repos { + if *repo.Name == projectName { + return &repo, nil + } + } + + return nil, fmt.Errorf("error finding default git repository in project %s", projectName) +} + +// prompt the user to select a repo and return a repository object +func GetAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { + gitClient, err := git.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + includeLinks := true + includeAllUrls := true + repoArgs := git.GetRepositoriesArgs{ + Project: &projectName, + IncludeLinks: &includeLinks, + IncludeAllUrls: &includeAllUrls, + } + + getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) + if err != nil { + return nil, err + } + repos := *getRepositoriesResult + + options := make([]string, len(repos)) + for idx, repo := range repos { + options[idx] = *repo.Name + } + repoIdx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Please choose an existing Azure DevOps Repository", + Options: options, + }) + + if err != nil { + return nil, fmt.Errorf("prompting for azdo project: %w", err) + } + selectedRepoName := options[repoIdx] + for _, repo := range repos { + if selectedRepoName == *repo.Name { + return &repo, nil + } + } + + return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) +} diff --git a/cli/azd/pkg/azdo/service_connection.go b/cli/azd/pkg/azdo/service_connection.go new file mode 100644 index 00000000000..ce830051046 --- /dev/null +++ b/cli/azd/pkg/azdo/service_connection.go @@ -0,0 +1,109 @@ +package azdo + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/build" + "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" +) + +// authorize a service connection to be used in all pipelines +func authorizeServiceConnectionToAllPipelines( + ctx context.Context, + projectId string, + endpoint *serviceendpoint.ServiceEndpoint, + connection *azuredevops.Connection) error { + buildClient, err := build.NewClient(ctx, connection) + if err != nil { + return err + } + + endpointResource := "endpoint" + endpointAuthorized := true + endpointId := endpoint.Id.String() + resources := make([]build.DefinitionResourceReference, 1) + resources[0] = build.DefinitionResourceReference{ + Type: &endpointResource, + Authorized: &endpointAuthorized, + Id: &endpointId, + } + + authorizeProjectResourcesArgs := build.AuthorizeProjectResourcesArgs{ + Project: &projectId, + Resources: &resources, + } + + _, err = buildClient.AuthorizeProjectResources(ctx, authorizeProjectResourcesArgs) + + if err != nil { + return err + } + return nil +} + +// create a new service connection that will be used in the deployment pipeline +func CreateServiceConnection( + ctx context.Context, + connection *azuredevops.Connection, + projectId string, + azdEnvironment environment.Environment, + credentials AzureServicePrincipalCredentials, + console input.Console) error { + + client, err := serviceendpoint.NewClient(ctx, connection) + if err != nil { + return err + } + + endpointType := "azurerm" + endpointOwner := "library" + endpointUrl := "https://management.azure.com/" + endpointName := ServiceConnectionName + endpointIsShared := false + endpointScheme := "ServicePrincipal" + + endpointAuthorizationParameters := make(map[string]string) + endpointAuthorizationParameters["serviceprincipalid"] = credentials.ClientId + endpointAuthorizationParameters["serviceprincipalkey"] = credentials.ClientSecret + endpointAuthorizationParameters["authenticationType"] = "spnKey" + endpointAuthorizationParameters["tenantid"] = credentials.TenantId + + endpointData := make(map[string]string) + endpointData["environment"] = CloudEnvironment + endpointData["subscriptionId"] = credentials.SubscriptionId + endpointData["subscriptionName"] = "azure subscription" + endpointData["scopeLevel"] = "Subscription" + endpointData["creationMode"] = "Manual" + + endpointAuthorization := serviceendpoint.EndpointAuthorization{ + Scheme: &endpointScheme, + Parameters: &endpointAuthorizationParameters, + } + serviceEndpoint := &serviceendpoint.ServiceEndpoint{ + Type: &endpointType, + Owner: &endpointOwner, + Url: &endpointUrl, + Name: &endpointName, + IsShared: &endpointIsShared, + Authorization: &endpointAuthorization, + Data: &endpointData, + } + createServiceEndpointArgs := serviceendpoint.CreateServiceEndpointArgs{ + Project: &projectId, + Endpoint: serviceEndpoint, + } + + endpoint, err := client.CreateServiceEndpoint(ctx, createServiceEndpointArgs) + if err != nil { + return err + } + + err = authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) + if err != nil { + return err + } + return nil +} diff --git a/cli/azd/pkg/azdo/utils.go b/cli/azd/pkg/azdo/utils.go new file mode 100644 index 00000000000..86a359a0f0b --- /dev/null +++ b/cli/azd/pkg/azdo/utils.go @@ -0,0 +1,33 @@ +package azdo + +import ( + "context" + "fmt" + "os" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" +) + +// helper method to verify that a configuration exists in the .env file or in system environment variables +func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, key string, label string) (string, error) { + value := env.Values[key] + if value != "" { + return value, nil + } + + value, exists := os.LookupEnv(key) + if !exists || value == "" { + return value, fmt.Errorf("%s not found in environment variable %s", label, key) + } + return value, nil +} + +// helper method to ensure an Azure DevOps PAT exists either in .env or system environment variables +func EnsureAzdoPatExists(ctx context.Context, env *environment.Environment) (string, error) { + return ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") +} + +// helper method to ensure an Azure DevOps organization name exists either in .env or system environment variables +func EnsureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { + return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") +} diff --git a/cli/azd/pkg/commands/pipeline/azdo.go b/cli/azd/pkg/commands/pipeline/azdo.go deleted file mode 100644 index 4db553acbe4..00000000000 --- a/cli/azd/pkg/commands/pipeline/azdo.go +++ /dev/null @@ -1,665 +0,0 @@ -package pipeline - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/azure/azure-dev/cli/azd/pkg/environment" - "github.com/azure/azure-dev/cli/azd/pkg/input" - - "github.com/microsoft/azure-devops-go-api/azuredevops" - "github.com/microsoft/azure-devops-go-api/azuredevops/build" - "github.com/microsoft/azure-devops-go-api/azuredevops/core" - "github.com/microsoft/azure-devops-go-api/azuredevops/git" - "github.com/microsoft/azure-devops-go-api/azuredevops/operations" - "github.com/microsoft/azure-devops-go-api/azuredevops/policy" - "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" - "github.com/microsoft/azure-devops-go-api/azuredevops/taskagent" -) - -var ( - AzDoHostName = "dev.azure.com" // hostname of the AzDo PaaS service. - AzDoPatName = "AZURE_DEVOPS_EXT_PAT" // environment variable that holds the Azure DevOps PAT - AzDoEnvironmentOrgName = "AZURE_DEVOPS_ORG_NAME" // environment variable that holds the Azure DevOps Organization Name - AzDoEnvironmentProjectIdName = "AZURE_DEVOPS_PROJECT_ID" // Environment Configuration name used to store the project Id - AzDoEnvironmentProjectName = "AZURE_DEVOPS_PROJECT_NAME" // Environment Configuration name used to store the project name - AzDoEnvironmentRepoIdName = "AZURE_DEVOPS_REPOSITORY_ID" // Environment Configuration name used to store repo ID - AzDoEnvironmentRepoName = "AZURE_DEVOPS_REPOSITORY_NAME" // Environment Configuration name used to store the Repo Name - AzDoEnvironmentRepoWebUrl = "AZURE_DEVOPS_REPOSITORY_WEB_URL" // web url for the configured repo. This is displayed on a the command line after a successful invocation of azd pipeline config - AzdoConfigSuccessMessage = "\nSuccessfully configured Azure DevOps Repository %s\n" // success message after azd pipeline config is successful - AzurePipelineName = "Azure Dev Deploy" // name of the azure pipeline that will be created - AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" // path to the azure pipeline yaml - CloudEnvironment = "AzureCloud" // target Azure Cloud - DefaultBranch = "master" // default branch for pipeline and branch policy - AzDoProjectDescription = "Azure Dev CLI Project" // azure devops project description - ServiceConnectionName = "azconnection" // name of the service connection that will be used in the AzDo project. This will store the Azure service principal -) - -// helper method to verify that a configuration exists in the .env file or in system environment variables -func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, key string, label string) (string, error) { - value := env.Values[key] - if value != "" { - return value, nil - } - - value, exists := os.LookupEnv(key) - if !exists || value == "" { - return value, fmt.Errorf("%s not found in environment variable %s", label, key) - } - return value, nil -} - -// helper method to ensure an Azure DevOps PAT exists either in .env or system environment variables -func ensureAzdoPatExists(ctx context.Context, env *environment.Environment) (string, error) { - return ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") -} - -// helper method to ensure an Azure DevOps organization name exists either in .env or system environment variables -func ensureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { - return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") -} - -// helper method to return an Azure DevOps connection used the AzDo go sdk -func getAzdoConnection(ctx context.Context, organization string, personalAccessToken string) *azuredevops.Connection { - organizationUrl := fmt.Sprintf("https://%s/%s", AzDoHostName, organization) - connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken) - return connection -} - -// returns a default repo from a newly created AzDo project. -// this relies on the fact that new projects automatically get a repo named the same as the project -func getAzDoDefaultGitRepositoriesInProject(ctx context.Context, projectName string, connection *azuredevops.Connection) (*git.GitRepository, error) { - gitClient, err := git.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - includeLinks := true - includeAllUrls := true - repoArgs := git.GetRepositoriesArgs{ - Project: &projectName, - IncludeLinks: &includeLinks, - IncludeAllUrls: &includeAllUrls, - } - - getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) - if err != nil { - return nil, err - } - repos := *getRepositoriesResult - - for _, repo := range repos { - if *repo.Name == projectName { - return &repo, nil - } - } - - return nil, fmt.Errorf("error finding default git repository in project %s", projectName) -} - -// prompt the user to select a repo and return a repository object -func getAzDoGitRepositoriesInProject(ctx context.Context, projectName string, orgName string, connection *azuredevops.Connection, console input.Console) (*git.GitRepository, error) { - gitClient, err := git.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - includeLinks := true - includeAllUrls := true - repoArgs := git.GetRepositoriesArgs{ - Project: &projectName, - IncludeLinks: &includeLinks, - IncludeAllUrls: &includeAllUrls, - } - - getRepositoriesResult, err := gitClient.GetRepositories(ctx, repoArgs) - if err != nil { - return nil, err - } - repos := *getRepositoriesResult - - options := make([]string, len(repos)) - for idx, repo := range repos { - options[idx] = *repo.Name - } - repoIdx, err := console.Select(ctx, input.ConsoleOptions{ - Message: "Please choose an existing Azure DevOps Repository", - Options: options, - }) - - if err != nil { - return nil, fmt.Errorf("prompting for azdo project: %w", err) - } - selectedRepoName := options[repoIdx] - for _, repo := range repos { - if selectedRepoName == *repo.Name { - return &repo, nil - } - } - - return nil, fmt.Errorf("error finding git repository %s in organization %s", selectedRepoName, orgName) -} - -// create a new repository in the current project -func createRepository(ctx context.Context, projectId string, repoName string, connection *azuredevops.Connection) (*git.GitRepository, error) { - gitClient, err := git.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - gitRepositoryCreateOptions := git.GitRepositoryCreateOptions{ - Name: &repoName, - } - - createRepositoryArgs := git.CreateRepositoryArgs{ - Project: &projectId, - GitRepositoryToCreate: &gitRepositoryCreateOptions, - } - repo, err := gitClient.CreateRepository(ctx, createRepositoryArgs) - if err != nil { - return nil, err - } - return repo, nil -} - -// returns a process template (basic, agile etc) used in the new project creation flow -func getProcessTemplateId(ctx context.Context, client core.Client) (string, error) { - processArgs := core.GetProcessesArgs{} - processes, err := client.GetProcesses(ctx, processArgs) - if err != nil { - return "", err - } - process := (*processes)[0] - return process.Id.String(), nil -} - -// creates a new Azure Devops project -func createProject(ctx context.Context, connection *azuredevops.Connection, name string, description string, console input.Console) (*core.TeamProjectReference, error) { - coreClient, err := core.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - processTemplateId, err := getProcessTemplateId(ctx, coreClient) - if err != nil { - return nil, fmt.Errorf("error fetching process template id %w", err) - } - - capabilities := map[string]map[string]string{ - "versioncontrol": { - "sourceControlType": "git", - }, - "processTemplate": { - "templateTypeId": processTemplateId, - }, - } - args := core.QueueCreateProjectArgs{ - ProjectToCreate: &core.TeamProject{ - Description: &description, - Name: &name, - Visibility: &core.ProjectVisibilityValues.Private, - Capabilities: &capabilities, - }, - } - res, err := coreClient.QueueCreateProject(ctx, args) - if err != nil { - return nil, err - } - - operationsClient := operations.NewClient(ctx, connection) - - getOperationsArgs := operations.GetOperationArgs{ - OperationId: res.Id, - } - - projectCreated := false - maxCheck := 10 - count := 0 - - for !projectCreated { - operation, err := operationsClient.GetOperation(ctx, getOperationsArgs) - if err != nil { - return nil, err - } - - if *operation.Status == "succeeded" { - projectCreated = true - } - - if count >= maxCheck { - return nil, fmt.Errorf("error creating azure devops project %s", name) - } - - count++ - time.Sleep(700 * time.Millisecond) - } - - project, err := getAzdoProjectByName(ctx, connection, name) - if err != nil { - return nil, err - } - return project, nil -} - -// prompts the user for a new AzDo project name and creates the project -// returns project name, project id, error -func getAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azuredevops.Connection, env *environment.Environment, console input.Console) (string, string, error) { - var project *core.TeamProjectReference - currentFolderName := filepath.Base(repoPath) - var projectDescription string = AzDoProjectDescription - - for { - name, err := console.Prompt(ctx, input.ConsoleOptions{ - Message: "Enter the name for your new Azure Devops Project OR Hit enter to use this name:", - DefaultValue: currentFolderName, - }) - if err != nil { - return "", "", fmt.Errorf("asking for new project name: %w", err) - } - var message string = "" - newProject, err := createProject(ctx, connection, name, projectDescription, console) - if err != nil { - message = err.Error() - } - if strings.Contains(message, fmt.Sprintf("The following project already exists on the Azure DevOps Server: %s", name)) { - console.Message(ctx, fmt.Sprintf("error: the project name '%s' is already in use\n", name)) - continue // try again - } else if strings.Contains(message, "The following name is not valid") { - console.Message(ctx, fmt.Sprintf("error: the project name '%s' is not a valid Azure DevOps project Name. See https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops#project-names\n", name)) - continue // try again - } else if err != nil { - return "", "", fmt.Errorf("creating project: %w", err) - } else { - project = newProject - break - } - } - - return *project.Name, project.Id.String(), nil -} - -// return an azdo project by name -func getAzdoProjectByName(ctx context.Context, connection *azuredevops.Connection, name string) (*core.TeamProjectReference, error) { - coreClient, err := core.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - args := core.GetProjectsArgs{} - getProjectsResponse, err := coreClient.GetProjects(ctx, args) - if err != nil { - return nil, err - } - - projects := getProjectsResponse.Value - for _, project := range projects { - if *project.Name == name { - return &project, nil - } - } - - return nil, fmt.Errorf("azure devops project %s not found", name) -} - -// prompt the user to select form a list of existing Azure DevOps projects -func getAzdoProjectFromExisting(ctx context.Context, connection *azuredevops.Connection, console input.Console) (string, string, error) { - coreClient, err := core.NewClient(ctx, connection) - if err != nil { - return "", "", err - } - - args := core.GetProjectsArgs{} - getProjectsResponse, err := coreClient.GetProjects(ctx, args) - if err != nil { - return "", "", err - } - - projects := getProjectsResponse.Value - projectsList := make([]core.TeamProjectReference, len(projects)) - options := make([]string, len(projects)) - for idx, project := range projects { - options[idx] = *project.Name - projectsList[idx] = project - } - - projectIdx, err := console.Select(ctx, input.ConsoleOptions{ - Message: "Please choose an existing Azure DevOps Project", - Options: options, - }) - - if err != nil { - return "", "", fmt.Errorf("prompting for azdo project: %w", err) - } - - return options[projectIdx], projectsList[projectIdx].Id.String(), nil -} - -// Creates a variable to be associated with a Pipeline -func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bool) build.BuildDefinitionVariable { - return build.BuildDefinitionVariable{ - AllowOverride: &allowOverride, - IsSecret: &isSecret, - Value: &value, - } -} - -// returns the default agent queue. This is used to associate a Pipeline with a default agent pool queue -func getAgentQueue(ctx context.Context, projectId string, connection *azuredevops.Connection) (*taskagent.TaskAgentQueue, error) { - client, err := taskagent.NewClient(ctx, connection) - if err != nil { - return nil, err - } - getAgentQueuesArgs := taskagent.GetAgentQueuesArgs{ - Project: &projectId, - } - queues, err := client.GetAgentQueues(ctx, getAgentQueuesArgs) - if err != nil { - return nil, err - } - for _, queue := range *queues { - if *queue.Name == "Default" { - return &queue, nil - } - } - return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) -} - -// create a new Azure DevOps pipeline -func createPipeline( - ctx context.Context, - projectId string, - name string, - repoName string, - connection *azuredevops.Connection, - credentials AzureServicePrincipalCredentials, - env environment.Environment) (*build.BuildDefinition, error) { - - client, err := build.NewClient(ctx, connection) - if err != nil { - return nil, err - } - - repoType := "tfsgit" - buildDefinitionType := build.DefinitionType("build") - definitionQueueStatus := build.DefinitionQueueStatus("enabled") - defaultBranch := fmt.Sprintf("refs/heads/%s", DefaultBranch) - buildRepository := &build.BuildRepository{ - Type: &repoType, - Name: &repoName, - DefaultBranch: &defaultBranch, - } - - process := make(map[string]interface{}) - process["type"] = 2 - process["yamlFilename"] = AzurePipelineYamlPath - - variables := make(map[string]build.BuildDefinitionVariable) - variables["AZURE_SUBSCRIPTION_ID"] = createBuildDefinitionVariable(credentials.SubscriptionId, false, false) - variables["ARM_TENANT_ID"] = createBuildDefinitionVariable(credentials.TenantId, false, false) - variables["ARM_CLIENT_ID"] = createBuildDefinitionVariable(credentials.ClientId, true, false) - variables["ARM_CLIENT_SECRET"] = createBuildDefinitionVariable(credentials.ClientSecret, true, false) - variables["AZURE_LOCATION"] = createBuildDefinitionVariable(env.GetLocation(), false, false) - variables["AZURE_ENV_NAME"] = createBuildDefinitionVariable(env.GetEnvName(), false, false) - - queue, err := getAgentQueue(ctx, projectId, connection) - if err != nil { - return nil, err - } - - agentPoolQueue := &build.AgentPoolQueue{ - Id: queue.Id, - Name: queue.Name, - } - - trigger := make(map[string]interface{}) - trigger["batchChanges"] = false - trigger["maxConcurrentBuildsPerBranch"] = 1 - trigger["pollingInterval"] = 0 - trigger["isSettingsSourceOptionSupported"] = true - trigger["defaultSettingsSourceType"] = 2 - trigger["settingsSourceType"] = 2 - trigger["defaultSettingsSourceType"] = 2 - trigger["triggerType"] = 2 - - triggers := make([]interface{}, 1) - triggers[0] = trigger - - buildDefinition := &build.BuildDefinition{ - Name: &name, - Type: &buildDefinitionType, - QueueStatus: &definitionQueueStatus, - Repository: buildRepository, - Process: process, - Queue: agentPoolQueue, - Variables: &variables, - Triggers: &triggers, - } - - createDefinitionArgs := &build.CreateDefinitionArgs{ - Project: &projectId, - Definition: buildDefinition, - } - - newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) - if err != nil { - return nil, err - } - - return newBuildDefinition, nil -} - -// run a pipeline. This is used to invoke the deploy pipeline after a successful push of the code -func queueBuild( - ctx context.Context, - connection *azuredevops.Connection, - projectId string, - buildDefinition *build.BuildDefinition) error { - client, err := build.NewClient(ctx, connection) - if err != nil { - return err - } - definitionReference := &build.DefinitionReference{ - Id: buildDefinition.Id, - } - - newBuild := &build.Build{ - Definition: definitionReference, - } - queueBuildArgs := build.QueueBuildArgs{ - Project: &projectId, - Build: newBuild, - } - - //time.Sleep(500 * time.Millisecond) - - _, err = client.QueueBuild(ctx, queueBuildArgs) - if err != nil { - return err - } - - return nil -} - -// authorize a service connection to be used in all pipelines -func authorizeServiceConnectionToAllPipelines( - ctx context.Context, - projectId string, - endpoint *serviceendpoint.ServiceEndpoint, - connection *azuredevops.Connection) error { - buildClient, err := build.NewClient(ctx, connection) - if err != nil { - return err - } - - endpointResource := "endpoint" - endpointAuthorized := true - endpointId := endpoint.Id.String() - resources := make([]build.DefinitionResourceReference, 1) - resources[0] = build.DefinitionResourceReference{ - Type: &endpointResource, - Authorized: &endpointAuthorized, - Id: &endpointId, - } - - authorizeProjectResourcesArgs := build.AuthorizeProjectResourcesArgs{ - Project: &projectId, - Resources: &resources, - } - - _, err = buildClient.AuthorizeProjectResources(ctx, authorizeProjectResourcesArgs) - - if err != nil { - return err - } - return nil -} - -// create a new service connection that will be used in the deployment pipeline -func createServiceConnection( - ctx context.Context, - connection *azuredevops.Connection, - projectId string, - azdEnvironment environment.Environment, - repoDetails *gitRepositoryDetails, - credentials AzureServicePrincipalCredentials, - console input.Console) error { - - client, err := serviceendpoint.NewClient(ctx, connection) - if err != nil { - return err - } - - endpointType := "azurerm" - endpointOwner := "library" - endpointUrl := "https://management.azure.com/" - endpointName := ServiceConnectionName - endpointIsShared := false - endpointScheme := "ServicePrincipal" - - endpointAuthorizationParameters := make(map[string]string) - endpointAuthorizationParameters["serviceprincipalid"] = credentials.ClientId - endpointAuthorizationParameters["serviceprincipalkey"] = credentials.ClientSecret - endpointAuthorizationParameters["authenticationType"] = "spnKey" - endpointAuthorizationParameters["tenantid"] = credentials.TenantId - - endpointData := make(map[string]string) - endpointData["environment"] = CloudEnvironment - endpointData["subscriptionId"] = credentials.SubscriptionId - endpointData["subscriptionName"] = "azure subscription" - endpointData["scopeLevel"] = "Subscription" - endpointData["creationMode"] = "Manual" - - endpointAuthorization := serviceendpoint.EndpointAuthorization{ - Scheme: &endpointScheme, - Parameters: &endpointAuthorizationParameters, - } - serviceEndpoint := &serviceendpoint.ServiceEndpoint{ - Type: &endpointType, - Owner: &endpointOwner, - Url: &endpointUrl, - Name: &endpointName, - IsShared: &endpointIsShared, - Authorization: &endpointAuthorization, - Data: &endpointData, - } - createServiceEndpointArgs := serviceendpoint.CreateServiceEndpointArgs{ - Project: &projectId, - Endpoint: serviceEndpoint, - } - - endpoint, err := client.CreateServiceEndpoint(ctx, createServiceEndpointArgs) - if err != nil { - return err - } - - err = authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) - if err != nil { - return err - } - return nil -} - -// returns a build policy type named "Build." Used to created the PR build policy on the default branch -func getBuildType(ctx context.Context, projectId *string, policyClient policy.Client) (*policy.PolicyType, error) { - getPolicyTypesArgs := policy.GetPolicyTypesArgs{ - Project: projectId, - } - policyTypes, err := policyClient.GetPolicyTypes(ctx, getPolicyTypesArgs) - if err != nil { - return nil, err - } - for _, policy := range *policyTypes { - if *policy.DisplayName == "Build" { - return &policy, nil - } - } - return nil, fmt.Errorf("could not find 'Build' policy type in project") -} - -// create the PR build policy to ensure that the pipeline runs on a new pull request -// this also disables direct pushes to the default branch and requires changes to go through a PR. -func createBuildPolicy( - ctx context.Context, - connection *azuredevops.Connection, - projectId string, - repoId string, - buildDefinition *build.BuildDefinition) error { - client, err := policy.NewClient(ctx, connection) - if err != nil { - return err - } - - buildPolicyType, err := getBuildType(ctx, &projectId, client) - if err != nil { - return err - } - - policyTypeRef := &policy.PolicyTypeRef{ - Id: buildPolicyType.Id, - } - policyRevision := 1 - policyIsDeleted := false - policyIsBlocking := true - policyIsEnabled := true - - policySettingsScope := make(map[string]interface{}) - policySettingsScope["repositoryId"] = repoId - policySettingsScope["refName"] = fmt.Sprintf("refs/heads/%s", DefaultBranch) - policySettingsScope["matchKind"] = "Exact" - - policySettingsScopes := make([]map[string]interface{}, 1) - policySettingsScopes[0] = policySettingsScope - - policySettings := make(map[string]interface{}) - policySettings["buildDefinitionId"] = buildDefinition.Id - policySettings["displayName"] = "Azure Dev Deploy PR" - policySettings["manualQueueOnly"] = false - policySettings["queueOnSourceUpdateOnly"] = true - policySettings["validDuration"] = 720 - policySettings["scope"] = policySettingsScopes - - policyConfiguration := &policy.PolicyConfiguration{ - Type: policyTypeRef, - Revision: &policyRevision, - IsDeleted: &policyIsDeleted, - IsBlocking: &policyIsBlocking, - IsEnabled: &policyIsEnabled, - Settings: policySettings, - } - - createPolicyConfigurationArgs := policy.CreatePolicyConfigurationArgs{ - Project: &projectId, - Configuration: policyConfiguration, - } - - _, err = client.CreatePolicyConfiguration(ctx, createPolicyConfigurationArgs) - if err != nil { - return err - } - - return nil -} diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 7e5a1752b75..9ea7254e39e 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -11,6 +11,7 @@ import ( "regexp" "strings" + "github.com/azure/azure-dev/cli/azd/pkg/azdo" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" @@ -57,12 +58,12 @@ func (p *AzdoHubScmProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck check the current state of external tools and any // other dependency to be as expected for execution. func (p *AzdoHubScmProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - _, err := ensureAzdoPatExists(ctx, p.Env) + _, err := azdo.EnsureAzdoPatExists(ctx, p.Env) if err != nil { return err } - _, err = ensureAzdoOrgNameExists(ctx, p.Env) + _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env) return err } @@ -92,17 +93,17 @@ func (p *AzdoHubScmProvider) StoreRepoDetails(ctx context.Context, repo *azdoGit repoDetails.sshUrl = *repo.SshUrl repoDetails.repoId = repo.Id.String() - err := p.saveEnvironmentConfig(AzDoEnvironmentRepoIdName, p.repoDetails.repoId) + err := p.saveEnvironmentConfig(azdo.AzDoEnvironmentRepoIdName, p.repoDetails.repoId) if err != nil { return fmt.Errorf("error saving repo id to environment %w", err) } - err = p.saveEnvironmentConfig(AzDoEnvironmentRepoName, p.repoDetails.repoName) + err = p.saveEnvironmentConfig(azdo.AzDoEnvironmentRepoName, p.repoDetails.repoName) if err != nil { return fmt.Errorf("error saving repo name to environment %w", err) } - err = p.saveEnvironmentConfig(AzDoEnvironmentRepoWebUrl, p.repoDetails.repoWebUrl) + err = p.saveEnvironmentConfig(azdo.AzDoEnvironmentRepoWebUrl, p.repoDetails.repoWebUrl) if err != nil { return fmt.Errorf("error saving repo web url to environment %w", err) } @@ -128,7 +129,7 @@ func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context } var message string - newRepo, err := createRepository(ctx, p.repoDetails.projectId, name, connection) + newRepo, err := azdo.CreateRepository(ctx, p.repoDetails.projectId, name, connection) if err != nil { message = err.Error() } @@ -136,7 +137,7 @@ func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context console.Message(ctx, fmt.Sprintf("error: the repo name '%s' is already in use\n", name)) continue // try again } else if strings.Contains(message, "TF401025: 'repoName' is not a valid name for a Git repository.") { - console.Message(ctx, fmt.Sprintf("error: '%s' is not a valid Azure Devops repo name. See https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops#azure-repos-git\n", name)) + console.Message(ctx, fmt.Sprintf("error: '%s' is not a valid Azure Devops repo name. See https://aka.ms/azure-dev/azdo-repo-naming\n", name)) continue // try again } else if err != nil { return "", fmt.Errorf("creating repository: %w", err) @@ -165,7 +166,7 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons return "", err } - repo, err := getAzDoGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console) + repo, err := azdo.GetAzDoGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console) if err != nil { return "", err } @@ -181,7 +182,7 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons } remoteUser := remoteParts[0] remoteHost := remoteParts[1] - pat, err := ensureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) if err != nil { return "", err } @@ -208,7 +209,7 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop return p.azdoConnection, nil } - org, err := ensureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) if err != nil { return nil, err } @@ -216,12 +217,16 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop repoDetails := p.getRepoDetails() repoDetails.orgName = org - pat, err := ensureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + if err != nil { + return nil, err + } + + connection, err := azdo.GetAzdoConnection(ctx, org, pat) if err != nil { return nil, err } - connection := getAzdoConnection(ctx, org, pat) return connection, nil } @@ -253,13 +258,13 @@ func (p *AzdoHubScmProvider) ensureProjectExists(ctx context.Context, console in switch idx { // Select from an existing AzDo project case 0: - projectName, projectId, err = getAzdoProjectFromExisting(ctx, connection, console) + projectName, projectId, err = azdo.GetAzdoProjectFromExisting(ctx, connection, console) if err != nil { return "", "", false, err } // Create a new AzDo project case 1: - projectName, projectId, err = getAzdoProjectFromNew(ctx, p.AzdContext.ProjectDirectory(), connection, p.Env, console) + projectName, projectId, err = azdo.GetAzdoProjectFromNew(ctx, p.AzdContext.ProjectDirectory(), connection, p.Env, console) newProject = true if err != nil { return "", "", false, err @@ -281,12 +286,12 @@ func (p *AzdoHubScmProvider) configureGitRemote(ctx context.Context, repoPath st repoDetails.projectName = projectName repoDetails.projectId = projectId - err = p.saveEnvironmentConfig(AzDoEnvironmentProjectIdName, projectId) + err = p.saveEnvironmentConfig(azdo.AzDoEnvironmentProjectIdName, projectId) if err != nil { return "", fmt.Errorf("error saving project id to environment %w", err) } - err = p.saveEnvironmentConfig(AzDoEnvironmentProjectName, projectName) + err = p.saveEnvironmentConfig(azdo.AzDoEnvironmentProjectName, projectName) if err != nil { return "", fmt.Errorf("error saving project name to environment %w", err) } @@ -312,7 +317,7 @@ func (p *AzdoHubScmProvider) getDefaultRepoRemote(ctx context.Context, projectNa if err != nil { return "", err } - repo, err := getAzDoDefaultGitRepositoriesInProject(ctx, projectName, connection) + repo, err := azdo.GetAzDoDefaultGitRepositoriesInProject(ctx, projectName, connection) if err != nil { return "", err } @@ -402,22 +407,22 @@ func (p *AzdoHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl strin repoDetails := p.getRepoDetails() if repoDetails.orgName == "" { - repoDetails.orgName = p.Env.Values[AzDoEnvironmentOrgName] + repoDetails.orgName = p.Env.Values[azdo.AzDoEnvironmentOrgName] } if repoDetails.projectName == "" { - repoDetails.projectName = p.Env.Values[AzDoEnvironmentProjectName] + repoDetails.projectName = p.Env.Values[azdo.AzDoEnvironmentProjectName] } if repoDetails.projectId == "" { - repoDetails.projectId = p.Env.Values[AzDoEnvironmentProjectIdName] + repoDetails.projectId = p.Env.Values[azdo.AzDoEnvironmentProjectIdName] } if repoDetails.repoName == "" { - repoDetails.repoName = p.Env.Values[AzDoEnvironmentRepoName] + repoDetails.repoName = p.Env.Values[azdo.AzDoEnvironmentRepoName] } if repoDetails.repoId == "" { - repoDetails.repoId = p.Env.Values[AzDoEnvironmentRepoIdName] + repoDetails.repoId = p.Env.Values[azdo.AzDoEnvironmentRepoIdName] } if repoDetails.repoWebUrl == "" { - repoDetails.repoWebUrl = p.Env.Values[AzDoEnvironmentRepoWebUrl] + repoDetails.repoWebUrl = p.Env.Values[azdo.AzDoEnvironmentRepoWebUrl] } if repoDetails.remoteUrl == "" { repoDetails.remoteUrl = remoteUrl @@ -448,7 +453,7 @@ func (p *AzdoHubScmProvider) preventGitPush( console.Message(ctx, ` A credential helper is not configured for git. This will require you to enter your Azure DevOps PAT when executing a git push. - https://git-scm.com/docs/git-credential-store + https://aka.ms/azure-dev/git-store `) idx, err := console.Select(ctx, input.ConsoleOptions{ Message: "Would you like to enable credential.helper store for this local git repository?", @@ -497,7 +502,7 @@ func (p *AzdoHubScmProvider) postGitPush( return err } if gitRepo.pushStatus { - console.Message(ctx, output.WithSuccessFormat(AzdoConfigSuccessMessage, p.repoDetails.repoWebUrl)) + console.Message(ctx, output.WithSuccessFormat(azdo.AzdoConfigSuccessMessage, p.repoDetails.repoWebUrl)) } connection, err := p.getAzdoConnection(ctx) @@ -505,12 +510,12 @@ func (p *AzdoHubScmProvider) postGitPush( return err } - err = createBuildPolicy(ctx, connection, p.repoDetails.projectId, p.repoDetails.repoId, p.repoDetails.buildDefinition) + err = azdo.CreateBuildPolicy(ctx, connection, p.repoDetails.projectId, p.repoDetails.repoId, p.repoDetails.buildDefinition) if err != nil { return err } - err = queueBuild(ctx, connection, p.repoDetails.projectId, p.repoDetails.buildDefinition) + err = azdo.QueueBuild(ctx, connection, p.repoDetails.projectId, p.repoDetails.buildDefinition) if err != nil { return err } @@ -522,7 +527,7 @@ func (p *AzdoHubScmProvider) postGitPush( type AzdoCiProvider struct { Env *environment.Environment AzdContext *azdcontext.AzdContext - credentials *AzureServicePrincipalCredentials + credentials *azdo.AzureServicePrincipalCredentials } // *** subareaProvider implementation ****** @@ -534,12 +539,12 @@ func (p *AzdoCiProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck nil for Azdo func (p *AzdoCiProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - _, err := ensureAzdoPatExists(ctx, p.Env) + _, err := azdo.EnsureAzdoPatExists(ctx, p.Env) if err != nil { return err } - _, err = ensureAzdoOrgNameExists(ctx, p.Env) + _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env) return err } @@ -566,17 +571,19 @@ func (p *AzdoCiProvider) configureConnection( p.credentials = azureCredentials details := repoDetails.details.(*AzdoRepositoryDetails) - org, err := ensureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) if err != nil { return err } - pat, err := ensureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) if err != nil { return err } - connection := getAzdoConnection(ctx, org, pat) - - err = createServiceConnection(ctx, connection, details.projectId, *p.Env, repoDetails, *p.credentials, console) + connection, err := azdo.GetAzdoConnection(ctx, org, pat) + if err != nil { + return err + } + err = azdo.CreateServiceConnection(ctx, connection, details.projectId, *p.Env, *p.credentials, console) if err != nil { return err } @@ -584,16 +591,10 @@ func (p *AzdoCiProvider) configureConnection( } // struct used to deserialize service principal json object -type AzureServicePrincipalCredentials struct { - TenantId string `json:"tenantId"` - ClientId string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - SubscriptionId string `json:"subscriptionId"` -} // parses the incoming json object and deserializes it to a struct -func parseCredentials(ctx context.Context, credentials json.RawMessage) (*AzureServicePrincipalCredentials, error) { - azureCredentials := AzureServicePrincipalCredentials{} +func parseCredentials(ctx context.Context, credentials json.RawMessage) (*azdo.AzureServicePrincipalCredentials, error) { + azureCredentials := azdo.AzureServicePrincipalCredentials{} if e := json.Unmarshal(credentials, &azureCredentials); e != nil { return nil, fmt.Errorf("setting terraform env var credentials: %w", e) } @@ -604,17 +605,19 @@ func parseCredentials(ctx context.Context, credentials json.RawMessage) (*AzureS func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error { details := repoDetails.details.(*AzdoRepositoryDetails) - org, err := ensureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) if err != nil { return err } - pat, err := ensureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) if err != nil { return err } - connection := getAzdoConnection(ctx, org, pat) - - buildDefinition, err := createPipeline(ctx, details.projectId, AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) + connection, err := azdo.GetAzdoConnection(ctx, org, pat) + if err != nil { + return err + } + buildDefinition, err := azdo.CreatePipeline(ctx, details.projectId, azdo.AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) if err != nil { return err } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider_test.go b/cli/azd/pkg/commands/pipeline/azdo_provider_test.go new file mode 100644 index 00000000000..fc1abaff18d --- /dev/null +++ b/cli/azd/pkg/commands/pipeline/azdo_provider_test.go @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package pipeline + +import ( + "context" + "os" + "path" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/azdo" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/stretchr/testify/require" +) + +func Test_azdo_provider_getRepoDetails(t *testing.T) { + t.Run("error", func(t *testing.T) { + // arrange + provider := getAzdoScmProviderTestHarness() + testOrgName := provider.Env.Values[azdo.AzDoEnvironmentOrgName] + testRepoName := provider.Env.Values[azdo.AzDoEnvironmentRepoName] + ctx := context.Background() + + // act + details, e := provider.gitRepoDetails(ctx, "https://fake_org@dev.azure.com/fake_org/repo1/_git/repo1") + + // assert + require.NoError(t, e) + require.EqualValues(t, testOrgName, details.owner) + require.EqualValues(t, testRepoName, details.repoName) + require.EqualValues(t, false, details.pushStatus) + }) + + t.Run("ssh", func(t *testing.T) { + // arrange + provider := getAzdoScmProviderTestHarness() + testOrgName := provider.Env.Values[azdo.AzDoEnvironmentOrgName] + testRepoName := provider.Env.Values[azdo.AzDoEnvironmentRepoName] + ctx := context.Background() + + // act + details, e := provider.gitRepoDetails(ctx, "git@ssh.dev.azure.com:v3/fake_org/repo1/repo1") + + // assert + require.NoError(t, e) + require.EqualValues(t, testOrgName, details.owner) + require.EqualValues(t, testRepoName, details.repoName) + require.EqualValues(t, false, details.pushStatus) + }) + + t.Run("non azure devops https remote", func(t *testing.T) { + //arrange + provider := &AzdoHubScmProvider{} + ctx := context.Background() + + //act + details, e := provider.gitRepoDetails(ctx, "https://github.com/Azure/azure-dev.git") + + //asserts + require.Error(t, e, ErrRemoteHostIsNotAzDo) + require.EqualValues(t, (*gitRepositoryDetails)(nil), details) + }) + + t.Run("non azure devops git remote", func(t *testing.T) { + //arrange + provider := &AzdoHubScmProvider{} + ctx := context.Background() + + //act + details, e := provider.gitRepoDetails(ctx, "git@github.com:Azure/azure-dev.git") + + //asserts + require.Error(t, e, ErrRemoteHostIsNotAzDo) + require.EqualValues(t, (*gitRepositoryDetails)(nil), details) + }) +} + +func Test_azdo_provider_preConfigureCheck(t *testing.T) { + t.Run("accepts a PAT via system environment variables", func(t *testing.T) { + // arrange + testPat := "12345" + provider := getEmptyAzdoScmProviderTestHarness() + os.Setenv(azdo.AzDoEnvironmentOrgName, "testOrg") + os.Setenv(azdo.AzDoPatName, testPat) + testConsole := &circularConsole{} + ctx := context.Background() + + // act + e := provider.preConfigureCheck(ctx, testConsole) + + // assert + require.NoError(t, e) + + //cleanup + os.Unsetenv(azdo.AzDoPatName) + }) + + t.Run("returns an error if no pat is provided", func(t *testing.T) { + // arrange + os.Unsetenv(azdo.AzDoPatName) + os.Setenv(azdo.AzDoEnvironmentOrgName, "testOrg") + provider := getEmptyAzdoScmProviderTestHarness() + testConsole := &circularConsole{} + ctx := context.Background() + + // act + e := provider.preConfigureCheck(ctx, testConsole) + + // assert + require.Error(t, e) + }) + +} + +func Test_saveEnvironmentConfig(t *testing.T) { + tempDir := t.TempDir() + + t.Run("saves to environment file", func(t *testing.T) { + // arrange + key := "test" + value := "12345" + provider := getEmptyAzdoScmProviderTestHarness() + envPath := path.Join(tempDir, ".test.env") + provider.Env = environment.EmptyWithFile(envPath) + // act + e := provider.saveEnvironmentConfig(key, value) + // assert + writtenEnv, err := environment.FromFile(envPath) + require.NoError(t, err) + + require.EqualValues(t, writtenEnv.Values[key], value) + require.NoError(t, e) + }) + +} +func getEmptyAzdoScmProviderTestHarness() *AzdoHubScmProvider { + return &AzdoHubScmProvider{ + Env: &environment.Environment{ + Values: map[string]string{}, + }, + } +} + +func getAzdoScmProviderTestHarness() *AzdoHubScmProvider { + return &AzdoHubScmProvider{ + Env: &environment.Environment{ + Values: map[string]string{ + azdo.AzDoEnvironmentOrgName: "fake_org", + azdo.AzDoEnvironmentProjectName: "project1", + azdo.AzDoEnvironmentProjectIdName: "12345", + azdo.AzDoEnvironmentRepoName: "repo1", + azdo.AzDoEnvironmentRepoIdName: "9876", + azdo.AzDoEnvironmentRepoWebUrl: "https://repo", + }, + }, + } +} diff --git a/cli/azd/pkg/commands/pipeline/github_provider_test.go b/cli/azd/pkg/commands/pipeline/github_provider_test.go index c4e2b879b0f..d012ea513af 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider_test.go +++ b/cli/azd/pkg/commands/pipeline/github_provider_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_getRepoDetails(t *testing.T) { +func Test_gitHub_provider_getRepoDetails(t *testing.T) { t.Run("https", func(t *testing.T) { provider := &GitHubScmProvider{} ctx := context.Background() From c5acdee7c7de961b27a6bb5df1758ed298d5c2d8 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Mon, 19 Sep 2022 00:53:38 -0700 Subject: [PATCH 26/33] Adding build policy tests --- cli/azd/pkg/azdo/build_policy_test.go | 121 ++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 cli/azd/pkg/azdo/build_policy_test.go diff --git a/cli/azd/pkg/azdo/build_policy_test.go b/cli/azd/pkg/azdo/build_policy_test.go new file mode 100644 index 00000000000..db8f7196da3 --- /dev/null +++ b/cli/azd/pkg/azdo/build_policy_test.go @@ -0,0 +1,121 @@ +package azdo + +import ( + "context" + "testing" + + "github.com/microsoft/azure-devops-go-api/azuredevops/policy" + "github.com/stretchr/testify/require" +) + +func Test_getBuildType(t *testing.T) { + ctx := context.Background() + + t.Run("getPolicyTypesArgs contains projectId", func(t *testing.T) { + //arrange + mockClient := MockPolicyClient{} + projectId := "111222" + //act + policyType, err := getBuildType(ctx, &projectId, &mockClient) + //assert + require.NoError(t, err) + require.NotNil(t, policyType) + require.EqualValues(t, *mockClient.getPolicyTypesArgs.Project, projectId) + }) + + t.Run("returns only 'Build' policy", func(t *testing.T) { + //arrange + mockClient := MockPolicyClient{} + projectId := "111222" + //act + policyType, err := getBuildType(ctx, &projectId, &mockClient) + //assert + require.NoError(t, err) + require.NotNil(t, policyType) + require.EqualValues(t, *policyType.DisplayName, "Build") + + }) + +} + +type MockPolicyClient struct { + getPolicyTypesArgs policy.GetPolicyTypesArgs +} + +func (c *MockPolicyClient) CreatePolicyConfiguration( + context.Context, + policy.CreatePolicyConfigurationArgs) (*policy.PolicyConfiguration, error) { + return nil, nil +} + +func (c *MockPolicyClient) DeletePolicyConfiguration( + context.Context, + policy.DeletePolicyConfigurationArgs) error { + return nil +} + +func (c *MockPolicyClient) GetPolicyConfiguration( + context.Context, + policy.GetPolicyConfigurationArgs) (*policy.PolicyConfiguration, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyConfigurationRevision( + context.Context, + policy.GetPolicyConfigurationRevisionArgs) (*policy.PolicyConfiguration, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyConfigurationRevisions(context.Context, + policy.GetPolicyConfigurationRevisionsArgs) (*[]policy.PolicyConfiguration, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyConfigurations(context.Context, + policy.GetPolicyConfigurationsArgs) (*policy.GetPolicyConfigurationsResponseValue, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyEvaluation( + context.Context, + policy.GetPolicyEvaluationArgs) (*policy.PolicyEvaluationRecord, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyEvaluations( + context.Context, + policy.GetPolicyEvaluationsArgs) (*[]policy.PolicyEvaluationRecord, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyType( + context.Context, + policy.GetPolicyTypeArgs) (*policy.PolicyType, error) { + return nil, nil +} + +func (c *MockPolicyClient) GetPolicyTypes(ctx context.Context, args policy.GetPolicyTypesArgs) (*[]policy.PolicyType, error) { + c.getPolicyTypesArgs = args + policyTypes := make([]policy.PolicyType, 2) + buildPolicyType := "Build" + testPolicyType := "Test" + policyTypes[0] = policy.PolicyType{ + DisplayName: &buildPolicyType, + } + policyTypes[1] = policy.PolicyType{ + DisplayName: &testPolicyType, + } + return &policyTypes, nil +} + +func (c *MockPolicyClient) RequeuePolicyEvaluation( + context.Context, + policy.RequeuePolicyEvaluationArgs) (*policy.PolicyEvaluationRecord, error) { + return nil, nil +} + +func (c *MockPolicyClient) UpdatePolicyConfiguration( + context.Context, + policy.UpdatePolicyConfigurationArgs) (*policy.PolicyConfiguration, error) { + return nil, nil +} From 4c18f414db9500478cc4290ac6b02c56425b942e Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Mon, 19 Sep 2022 07:38:40 -0700 Subject: [PATCH 27/33] Added copyright and license comments to azdo package. --- cli/azd/pkg/azdo/azdo.go | 3 +++ cli/azd/pkg/azdo/azdo_test.go | 3 +++ cli/azd/pkg/azdo/build_policy.go | 3 +++ cli/azd/pkg/azdo/build_policy_test.go | 3 +++ cli/azd/pkg/azdo/pipeline.go | 3 +++ cli/azd/pkg/azdo/project.go | 3 +++ cli/azd/pkg/azdo/repository.go | 3 +++ cli/azd/pkg/azdo/service_connection.go | 3 +++ cli/azd/pkg/azdo/utils.go | 3 +++ 9 files changed, 27 insertions(+) diff --git a/cli/azd/pkg/azdo/azdo.go b/cli/azd/pkg/azdo/azdo.go index 7857b7f100e..83e569c7a2d 100644 --- a/cli/azd/pkg/azdo/azdo.go +++ b/cli/azd/pkg/azdo/azdo.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/azdo_test.go b/cli/azd/pkg/azdo/azdo_test.go index ef77a074e38..a5067a52d3d 100644 --- a/cli/azd/pkg/azdo/azdo_test.go +++ b/cli/azd/pkg/azdo/azdo_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/build_policy.go b/cli/azd/pkg/azdo/build_policy.go index 2ce1cdd7aad..62c7b77dcab 100644 --- a/cli/azd/pkg/azdo/build_policy.go +++ b/cli/azd/pkg/azdo/build_policy.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/build_policy_test.go b/cli/azd/pkg/azdo/build_policy_test.go index db8f7196da3..1408e95498d 100644 --- a/cli/azd/pkg/azdo/build_policy_test.go +++ b/cli/azd/pkg/azdo/build_policy_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/pipeline.go b/cli/azd/pkg/azdo/pipeline.go index 4ab8d0ea2a8..76c7166cc1b 100644 --- a/cli/azd/pkg/azdo/pipeline.go +++ b/cli/azd/pkg/azdo/pipeline.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/project.go b/cli/azd/pkg/azdo/project.go index 7ac6b2f2695..516750de030 100644 --- a/cli/azd/pkg/azdo/project.go +++ b/cli/azd/pkg/azdo/project.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/repository.go b/cli/azd/pkg/azdo/repository.go index 28b6ec4bd61..4d81285563d 100644 --- a/cli/azd/pkg/azdo/repository.go +++ b/cli/azd/pkg/azdo/repository.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/service_connection.go b/cli/azd/pkg/azdo/service_connection.go index ce830051046..381054ac81a 100644 --- a/cli/azd/pkg/azdo/service_connection.go +++ b/cli/azd/pkg/azdo/service_connection.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( diff --git a/cli/azd/pkg/azdo/utils.go b/cli/azd/pkg/azdo/utils.go index 86a359a0f0b..5cae1d5d066 100644 --- a/cli/azd/pkg/azdo/utils.go +++ b/cli/azd/pkg/azdo/utils.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package azdo import ( From 1e1e899443e036085f8d124582809b46e26a5741 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Mon, 19 Sep 2022 09:57:19 -0700 Subject: [PATCH 28/33] Modifying azd project description and existing sp env variable name. --- cli/azd/pkg/azdo/azdo.go | 2 +- cli/azd/pkg/commands/pipeline/pipeline_manager.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/azdo/azdo.go b/cli/azd/pkg/azdo/azdo.go index 83e569c7a2d..4ccd9dfdbd9 100644 --- a/cli/azd/pkg/azdo/azdo.go +++ b/cli/azd/pkg/azdo/azdo.go @@ -24,7 +24,7 @@ var ( AzurePipelineYamlPath = ".azdo/pipelines/azure-dev.yml" // path to the azure pipeline yaml CloudEnvironment = "AzureCloud" // target Azure Cloud DefaultBranch = "master" // default branch for pipeline and branch policy - AzDoProjectDescription = "Azure Dev CLI Project" // azure devops project description + AzDoProjectDescription = "Azure Developer CLI Project" // azure devops project description ServiceConnectionName = "azconnection" // name of the service connection that will be used in the AzDo project. This will store the Azure service principal ) diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 117d6711d4c..268c344508c 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -207,7 +207,7 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { var credentials json.RawMessage = nil var err error - existingSP := os.Getenv("AZD_EXISTING_SP") + existingSP := os.Getenv("AZURE_EXISTING_SP") if existingSP != "" { credentials = json.RawMessage(existingSP) } From 3f3927ecd5028094593ccd5ecee98433fe7d73d8 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Mon, 19 Sep 2022 12:24:15 -0700 Subject: [PATCH 29/33] Modifying existing sp env var to AZURE_CREDENTIALS --- cli/azd/pkg/commands/pipeline/pipeline_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 268c344508c..97937f38644 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -207,7 +207,7 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { var credentials json.RawMessage = nil var err error - existingSP := os.Getenv("AZURE_EXISTING_SP") + existingSP := os.Getenv("AZURE_CREDENTIALS") if existingSP != "" { credentials = json.RawMessage(existingSP) } From 920c0be048303f3b9e5d3b21b477c7baa3c0dcc0 Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 20 Sep 2022 18:26:05 -0700 Subject: [PATCH 30/33] Azdo demo refactor (#48) * Adding service connection name to pipeline variables * Checking for service connection exists and also removing do push condition * Check for pipeline exists and update the name to include repo if exists. * adding provider arg and setting variables only for terraform (#46) * Set default pipeline provider to github * Adjust project creation wait time. * prompt for pat and org name if not in system env vars. * Azure DevOps name standardization * Update Azdo readme. * refactor to fix failing test Co-authored-by: Victor Vazquez --- cli/azd/cmd/pipeline.go | 31 +- cli/azd/pkg/azdo/pipeline.go | 107 +++++- cli/azd/pkg/azdo/project.go | 2 +- cli/azd/pkg/azdo/service_connection.go | 77 ++++- cli/azd/pkg/azdo/utils.go | 79 ++++- .../pkg/commands/pipeline/azdo_provider.go | 41 +-- .../commands/pipeline/azdo_provider_test.go | 26 +- .../pkg/commands/pipeline/github_provider.go | 4 +- cli/azd/pkg/commands/pipeline/pipeline.go | 2 +- .../pkg/commands/pipeline/pipeline_manager.go | 22 +- templates/common/.azdo/pipelines/README.md | 307 +----------------- 11 files changed, 325 insertions(+), 373 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 2259c962bdc..49cc4c42369 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/commands" @@ -68,6 +69,7 @@ func (p *pipelineConfigAction) SetupFlags( local.StringVar(&p.manager.PipelineServicePrincipalName, "principal-name", "", "The name of the service principal to use to grant access to Azure resources as part of the pipeline.") local.StringVar(&p.manager.PipelineRemoteName, "remote-name", "origin", "The name of the git remote to configure the pipeline to run on.") local.StringVar(&p.manager.PipelineRoleName, "principal-role", "Contributor", "The role to assign to the service principal.") + local.StringVar(&p.manager.PipelineProvider, "provider", "github", "The pipeline provider to use (GitHub and Azdo supported).") } // Run implements action interface @@ -94,13 +96,28 @@ func (p *pipelineConfigAction) Run( return fmt.Errorf("loading environment: %w", err) } - // Detect the SCM and CI providers based on the project directory - p.manager.ScmProvider, - p.manager.CiProvider, - err = pipeline.DetectProviders(ctx, console, env) - - if err != nil { - return err + overrideProvider := strings.ToLower(p.manager.PipelineProvider) + if overrideProvider == "" { + // Detect the SCM and CI providers based on the project directory + p.manager.ScmProvider, + p.manager.CiProvider, + err = pipeline.DetectProviders(ctx, console, env) + if err != nil { + return err + } + } else if overrideProvider == "github" { + p.manager.ScmProvider = &pipeline.GitHubScmProvider{} + p.manager.CiProvider = &pipeline.GitHubCiProvider{} + } else if overrideProvider == "azdo" { + p.manager.ScmProvider = &pipeline.AzdoHubScmProvider{ + Env: env, + AzdContext: azdCtx} + p.manager.CiProvider = &pipeline.AzdoCiProvider{ + Env: env, + AzdContext: azdCtx, + } + } else { + return fmt.Errorf("Unknown pipeline provider: %s. Supported providers: [GitHub, Azdo]", overrideProvider) } // set context for manager diff --git a/cli/azd/pkg/azdo/pipeline.go b/cli/azd/pkg/azdo/pipeline.go index 76c7166cc1b..a63e9157dff 100644 --- a/cli/azd/pkg/azdo/pipeline.go +++ b/cli/azd/pkg/azdo/pipeline.go @@ -8,6 +8,8 @@ import ( "fmt" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" + "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/build" "github.com/microsoft/azure-devops-go-api/azuredevops/taskagent" @@ -43,6 +45,31 @@ func getAgentQueue(ctx context.Context, projectId string, connection *azuredevop return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) } +// find pipeline by name +func pipelineExists( + ctx context.Context, + client *build.Client, + projectId *string, + pipelineName *string, +) (bool, error) { + getDefinitionsArgs := build.GetDefinitionsArgs{ + Project: projectId, + Name: pipelineName, + } + + buildDefinitionsResponse, err := (*client).GetDefinitions(ctx, getDefinitionsArgs) + if err != nil { + return false, err + } + buildDefinitions := buildDefinitionsResponse.Value + for _, definition := range buildDefinitions { + if *definition.Name == *pipelineName { + return true, nil + } + } + return false, nil +} + // create a new Azure DevOps pipeline func CreatePipeline( ctx context.Context, @@ -51,13 +78,67 @@ func CreatePipeline( repoName string, connection *azuredevops.Connection, credentials AzureServicePrincipalCredentials, - env environment.Environment) (*build.BuildDefinition, error) { + env environment.Environment, + console input.Console, + provisioningProvider provisioning.Options) (*build.BuildDefinition, error) { client, err := build.NewClient(ctx, connection) if err != nil { return nil, err } + var exists bool = true + var count = 0 + var maxTries = 4 + for exists { + exists, err = pipelineExists(ctx, &client, &projectId, &name) + if err != nil { + return nil, err + } + count = count + 1 + + if exists { + name = fmt.Sprintf("%s - %s (%d)", name, repoName, count) + } else { + continue + } + + if count >= maxTries { + return nil, fmt.Errorf("error creating new pipeline") + } + } + + queue, err := getAgentQueue(ctx, projectId, connection) + if err != nil { + return nil, err + } + + createDefinitionArgs, err := createAzureDevPipelineArgs( + ctx, projectId, name, repoName, credentials, env, queue, provisioningProvider) + if err != nil { + return nil, err + } + + newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) + if err != nil { + return nil, err + } + + return newBuildDefinition, nil +} + +// create Azure Deploy Pipeline parameters +func createAzureDevPipelineArgs( + ctx context.Context, + projectId string, + name string, + repoName string, + credentials AzureServicePrincipalCredentials, + env environment.Environment, + queue *taskagent.TaskAgentQueue, + provisioningProvider provisioning.Options, +) (*build.CreateDefinitionArgs, error) { + repoType := "tfsgit" buildDefinitionType := build.DefinitionType("build") definitionQueueStatus := build.DefinitionQueueStatus("enabled") @@ -74,16 +155,14 @@ func CreatePipeline( variables := make(map[string]build.BuildDefinitionVariable) variables["AZURE_SUBSCRIPTION_ID"] = createBuildDefinitionVariable(credentials.SubscriptionId, false, false) - variables["ARM_TENANT_ID"] = createBuildDefinitionVariable(credentials.TenantId, false, false) - variables["ARM_CLIENT_ID"] = createBuildDefinitionVariable(credentials.ClientId, true, false) - variables["ARM_CLIENT_SECRET"] = createBuildDefinitionVariable(credentials.ClientSecret, true, false) + if provisioningProvider.Provider == provisioning.Terraform { + variables["ARM_TENANT_ID"] = createBuildDefinitionVariable(credentials.TenantId, false, false) + variables["ARM_CLIENT_ID"] = createBuildDefinitionVariable(credentials.ClientId, true, false) + variables["ARM_CLIENT_SECRET"] = createBuildDefinitionVariable(credentials.ClientSecret, true, false) + } variables["AZURE_LOCATION"] = createBuildDefinitionVariable(env.GetLocation(), false, false) variables["AZURE_ENV_NAME"] = createBuildDefinitionVariable(env.GetEnvName(), false, false) - - queue, err := getAgentQueue(ctx, projectId, connection) - if err != nil { - return nil, err - } + variables["AZURE_SERVICE_CONNECTION"] = createBuildDefinitionVariable(ServiceConnectionName, false, false) agentPoolQueue := &build.AgentPoolQueue{ Id: queue.Id, @@ -118,13 +197,7 @@ func CreatePipeline( Project: &projectId, Definition: buildDefinition, } - - newBuildDefinition, err := client.CreateDefinition(ctx, *createDefinitionArgs) - if err != nil { - return nil, err - } - - return newBuildDefinition, nil + return createDefinitionArgs, nil } // run a pipeline. This is used to invoke the deploy pipeline after a successful push of the code @@ -149,8 +222,6 @@ func QueueBuild( Build: newBuild, } - //time.Sleep(500 * time.Millisecond) - _, err = client.QueueBuild(ctx, queueBuildArgs) if err != nil { return err diff --git a/cli/azd/pkg/azdo/project.go b/cli/azd/pkg/azdo/project.go index 516750de030..56d6140cfe0 100644 --- a/cli/azd/pkg/azdo/project.go +++ b/cli/azd/pkg/azdo/project.go @@ -86,7 +86,7 @@ func createProject(ctx context.Context, connection *azuredevops.Connection, name } count++ - time.Sleep(700 * time.Millisecond) + time.Sleep(800 * time.Millisecond) } project, err := getAzdoProjectByName(ctx, connection, name) diff --git a/cli/azd/pkg/azdo/service_connection.go b/cli/azd/pkg/azdo/service_connection.go index 381054ac81a..77ae87ab51f 100644 --- a/cli/azd/pkg/azdo/service_connection.go +++ b/cli/azd/pkg/azdo/service_connection.go @@ -8,6 +8,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/build" "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" @@ -47,6 +48,33 @@ func authorizeServiceConnectionToAllPipelines( return nil } +// find service connection by name. +func serviceConnectionExists(ctx context.Context, + client *serviceendpoint.Client, + projectId *string, + serviceConnectionName *string) (bool, error) { + + endpointNames := make([]string, 1) + endpointNames[0] = *serviceConnectionName + getServiceEndpointsByNamesArgs := serviceendpoint.GetServiceEndpointsByNamesArgs{ + Project: projectId, + EndpointNames: &endpointNames, + } + + serviceEndpoints, err := (*client).GetServiceEndpointsByNames(ctx, getServiceEndpointsByNamesArgs) + if err != nil { + return false, err + } + + for _, endpoint := range *serviceEndpoints { + if *endpoint.Name == *serviceConnectionName && *endpoint.IsReady { + return true, nil + } + } + + return false, nil +} + // create a new service connection that will be used in the deployment pipeline func CreateServiceConnection( ctx context.Context, @@ -61,6 +89,40 @@ func CreateServiceConnection( return err } + foundServiceConnection, err := serviceConnectionExists(ctx, &client, &projectId, &ServiceConnectionName) + if err != nil { + return err + } + + // if a service connection exists, skip creation. + if foundServiceConnection { + console.Message(ctx, output.WithWarningFormat("Service Connection %s already exists. Skipping Service Connection Creation", ServiceConnectionName)) + return nil + } + + createServiceEndpointArgs, err := createAzureRMServiceEndPointArgs(ctx, &projectId, credentials) + if err != nil { + return err + } + endpoint, err := client.CreateServiceEndpoint(ctx, createServiceEndpointArgs) + if err != nil { + return err + } + + err = authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) + if err != nil { + return err + } + + return nil +} + +// creates input parameter needed to create the azure rm service connection +func createAzureRMServiceEndPointArgs( + ctx context.Context, + projectId *string, + credentials AzureServicePrincipalCredentials, +) (serviceendpoint.CreateServiceEndpointArgs, error) { endpointType := "azurerm" endpointOwner := "library" endpointUrl := "https://management.azure.com/" @@ -94,19 +156,10 @@ func CreateServiceConnection( Authorization: &endpointAuthorization, Data: &endpointData, } + createServiceEndpointArgs := serviceendpoint.CreateServiceEndpointArgs{ - Project: &projectId, + Project: projectId, Endpoint: serviceEndpoint, } - - endpoint, err := client.CreateServiceEndpoint(ctx, createServiceEndpointArgs) - if err != nil { - return err - } - - err = authorizeServiceConnectionToAllPipelines(ctx, projectId, endpoint, connection) - if err != nil { - return err - } - return nil + return createServiceEndpointArgs, nil } diff --git a/cli/azd/pkg/azdo/utils.go b/cli/azd/pkg/azdo/utils.go index 5cae1d5d066..cdcf3cce0e2 100644 --- a/cli/azd/pkg/azdo/utils.go +++ b/cli/azd/pkg/azdo/utils.go @@ -9,6 +9,8 @@ import ( "os" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" ) // helper method to verify that a configuration exists in the .env file or in system environment variables @@ -26,11 +28,80 @@ func ensureAzdoConfigExists(ctx context.Context, env *environment.Environment, k } // helper method to ensure an Azure DevOps PAT exists either in .env or system environment variables -func EnsureAzdoPatExists(ctx context.Context, env *environment.Environment) (string, error) { - return ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") +func EnsureAzdoPatExists(ctx context.Context, env *environment.Environment, console input.Console) (string, error) { + value, err := ensureAzdoConfigExists(ctx, env, AzDoPatName, "azure devops personal access token") + if err != nil { + console.Message(ctx, output.WithWarningFormat("You need an Azure DevOps Personal Access Token (PAT). Please create a PAT by following the instructions here https://aka.ms/azure-dev/azdo-pat")) + pat, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Personal Access Token (PAT):", + DefaultValue: "", + }) + if err != nil { + return "", fmt.Errorf("asking for pat: %w", err) + } + // set the pat as an environment variable for this cmd run + // note: the scope of this env var is only this shell invocation and won't be available in the caller parent shell + os.Setenv(AzDoPatName, pat) + value = pat + + idx, err := console.Select(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Would you like to PAT to the %s environment file (.env)?", env.GetEnvName()), + Options: []string{ + "Yes, save this PAT to the current .env file", + "No, do not store the PAT.", + }, + DefaultValue: "Yes, save this PAT to the current .env file", + }) + if err != nil { + return "", fmt.Errorf("prompting for pat storage: %w", err) + } + + switch idx { + // save pat to env file + case 0: + err = saveEnvironmentConfig(AzDoPatName, value, env) + if err != nil { + return "", err + } + // user rejected pat storage, skip + case 1: + break + default: + panic(fmt.Sprintf("unexpected selection index %d", idx)) + } + } + return value, nil } // helper method to ensure an Azure DevOps organization name exists either in .env or system environment variables -func EnsureAzdoOrgNameExists(ctx context.Context, env *environment.Environment) (string, error) { - return ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") +func EnsureAzdoOrgNameExists(ctx context.Context, env *environment.Environment, console input.Console) (string, error) { + value, err := ensureAzdoConfigExists(ctx, env, AzDoEnvironmentOrgName, "azure devops organization name") + if err != nil { + orgName, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Please enter an Azure DevOps Organization Name:", + DefaultValue: "", + }) + if err != nil { + return "", fmt.Errorf("asking for new project name: %w", err) + } + + err = saveEnvironmentConfig(AzDoEnvironmentOrgName, orgName, env) + if err != nil { + return "", err + } + + value = orgName + } + return value, nil +} + +// helper function to save configuration values to .env file +func saveEnvironmentConfig(key string, value string, env *environment.Environment) error { + env.Values[key] = value + err := env.Save() + + if err != nil { + return err + } + return nil } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider.go b/cli/azd/pkg/commands/pipeline/azdo_provider.go index 9ea7254e39e..6ccb1a03cb1 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider.go @@ -58,12 +58,12 @@ func (p *AzdoHubScmProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck check the current state of external tools and any // other dependency to be as expected for execution. func (p *AzdoHubScmProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - _, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + _, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return err } - _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env) + _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env, console) return err } @@ -137,7 +137,7 @@ func (p *AzdoHubScmProvider) createNewGitRepositoryFromInput(ctx context.Context console.Message(ctx, fmt.Sprintf("error: the repo name '%s' is already in use\n", name)) continue // try again } else if strings.Contains(message, "TF401025: 'repoName' is not a valid name for a Git repository.") { - console.Message(ctx, fmt.Sprintf("error: '%s' is not a valid Azure Devops repo name. See https://aka.ms/azure-dev/azdo-repo-naming\n", name)) + console.Message(ctx, fmt.Sprintf("error: '%s' is not a valid Azure DevOps repo name. See https://aka.ms/azure-dev/azdo-repo-naming\n", name)) continue // try again } else if err != nil { return "", fmt.Errorf("creating repository: %w", err) @@ -182,13 +182,13 @@ func (p *AzdoHubScmProvider) ensureGitRepositoryExists(ctx context.Context, cons } remoteUser := remoteParts[0] remoteHost := remoteParts[1] - pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return "", err } updatedRemote := fmt.Sprintf("%s:%s@%s", remoteUser, pat, remoteHost) - console.Message(ctx, fmt.Sprintf("using azure devOps repo: %s", p.repoDetails.repoWebUrl)) + console.Message(ctx, fmt.Sprintf("using Azure DevOps repo: %s", p.repoDetails.repoWebUrl)) return updatedRemote, nil } @@ -205,11 +205,12 @@ func (p *AzdoHubScmProvider) getRepoDetails() *AzdoRepositoryDetails { // helper function to return an azuredevops.Connection for use with AzDo Go SDK func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevops.Connection, error) { + console := input.GetConsole(ctx) if p.azdoConnection != nil { return p.azdoConnection, nil } - org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env, console) if err != nil { return nil, err } @@ -217,7 +218,7 @@ func (p *AzdoHubScmProvider) getAzdoConnection(ctx context.Context) (*azuredevop repoDetails := p.getRepoDetails() repoDetails.orgName = org - pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return nil, err } @@ -340,10 +341,10 @@ func (p *AzdoHubScmProvider) promptForAzdoRepository(ctx context.Context, consol idx, err := console.Select(ctx, input.ConsoleOptions{ Message: fmt.Sprintf("How would you like to configure your remote? (Organization: %s)", p.repoDetails.projectName), Options: []string{ - "Select an existing Azure Devops Repository", - "Create a new private Azure Devops Repository", + "Select an existing Azure DevOps Repository", + "Create a new private Azure DevOps Repository", }, - DefaultValue: "Create a new private Azure Devops Repository", + DefaultValue: "Create a new private Azure DevOps Repository", }) if err != nil { @@ -379,7 +380,7 @@ var azdoRemoteGitUrlRegex = regexp.MustCompile(`^git@ssh.dev.azure\.com:(.*?)(?: 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") +var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an Azure DevOps host") // helper function to determine if the provided remoteUrl is an azure devops repo. // currently supports AzDo PaaS @@ -539,12 +540,12 @@ func (p *AzdoCiProvider) requiredTools() []tools.ExternalTool { // preConfigureCheck nil for Azdo func (p *AzdoCiProvider) preConfigureCheck(ctx context.Context, console input.Console) error { - _, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + _, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return err } - _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env) + _, err = azdo.EnsureAzdoOrgNameExists(ctx, p.Env, console) return err } @@ -571,11 +572,11 @@ func (p *AzdoCiProvider) configureConnection( p.credentials = azureCredentials details := repoDetails.details.(*AzdoRepositoryDetails) - org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env, console) if err != nil { return err } - pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return err } @@ -602,14 +603,15 @@ func parseCredentials(ctx context.Context, credentials json.RawMessage) (*azdo.A } // configurePipeline create Azdo pipeline -func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error { +func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options) error { details := repoDetails.details.(*AzdoRepositoryDetails) + console := input.GetConsole(ctx) - org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env) + org, err := azdo.EnsureAzdoOrgNameExists(ctx, p.Env, console) if err != nil { return err } - pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env) + pat, err := azdo.EnsureAzdoPatExists(ctx, p.Env, console) if err != nil { return err } @@ -617,7 +619,8 @@ func (p *AzdoCiProvider) configurePipeline(ctx context.Context, repoDetails *git if err != nil { return err } - buildDefinition, err := azdo.CreatePipeline(ctx, details.projectId, azdo.AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env) + buildDefinition, err := azdo.CreatePipeline( + ctx, details.projectId, azdo.AzurePipelineName, details.repoName, connection, *p.credentials, *p.Env, console, provisioningProvider) if err != nil { return err } diff --git a/cli/azd/pkg/commands/pipeline/azdo_provider_test.go b/cli/azd/pkg/commands/pipeline/azdo_provider_test.go index fc1abaff18d..bdb060905d2 100644 --- a/cli/azd/pkg/commands/pipeline/azdo_provider_test.go +++ b/cli/azd/pkg/commands/pipeline/azdo_provider_test.go @@ -5,12 +5,14 @@ package pipeline import ( "context" + "io" "os" "path" "testing" "github.com/azure/azure-dev/cli/azd/pkg/azdo" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/stretchr/testify/require" ) @@ -101,14 +103,17 @@ func Test_azdo_provider_preConfigureCheck(t *testing.T) { os.Unsetenv(azdo.AzDoPatName) os.Setenv(azdo.AzDoEnvironmentOrgName, "testOrg") provider := getEmptyAzdoScmProviderTestHarness() - testConsole := &circularConsole{} + testConsole := &configurablePromptConsole{} + testPat := "testPAT12345" + testConsole.promptResponse = testPat ctx := context.Background() // act e := provider.preConfigureCheck(ctx, testConsole) // assert - require.Error(t, e) + require.Nil(t, e) + require.EqualValues(t, provider.Env.Values[azdo.AzDoPatName], testPat) }) } @@ -156,3 +161,20 @@ func getAzdoScmProviderTestHarness() *AzdoHubScmProvider { }, } } + +// For tests where the console won't matter at all +type configurablePromptConsole struct { + promptResponse string +} + +func (console *configurablePromptConsole) Message(ctx context.Context, message string) {} +func (console *configurablePromptConsole) Prompt(ctx context.Context, options input.ConsoleOptions) (string, error) { + return console.promptResponse, nil +} +func (console *configurablePromptConsole) Select(ctx context.Context, options input.ConsoleOptions) (int, error) { + return 0, nil +} +func (console *configurablePromptConsole) Confirm(ctx context.Context, options input.ConsoleOptions) (bool, error) { + return false, nil +} +func (console *configurablePromptConsole) SetWriter(writer io.Writer) {} diff --git a/cli/azd/pkg/commands/pipeline/github_provider.go b/cli/azd/pkg/commands/pipeline/github_provider.go index 3adb89785b7..327b54ed67a 100644 --- a/cli/azd/pkg/commands/pipeline/github_provider.go +++ b/cli/azd/pkg/commands/pipeline/github_provider.go @@ -321,7 +321,7 @@ func (p *GitHubCiProvider) configureConnection( return fmt.Errorf("failed setting AZURE_CREDENTIALS secret: %w", err) } - if infraOptions.Provider == "terraform" { + if infraOptions.Provider == provisioning.Terraform { // terraform expect the credential info to be set in the env individually type credentialParse struct { Tenant string `json:"tenantId"` @@ -379,7 +379,7 @@ func (p *GitHubCiProvider) configureConnection( // configurePipeline is a no-op for GitHub, as the pipeline is automatically // created by creating the workflow files in .github folder. -func (p *GitHubCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error { +func (p *GitHubCiProvider) configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options) error { return nil } diff --git a/cli/azd/pkg/commands/pipeline/pipeline.go b/cli/azd/pkg/commands/pipeline/pipeline.go index 235cc27a1eb..7c958fc4ce4 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline.go +++ b/cli/azd/pkg/commands/pipeline/pipeline.go @@ -75,7 +75,7 @@ type CiProvider interface { // compose the behavior from subareaProvider subareaProvider // configurePipeline set up or create the CI pipeline. - configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails) error + configurePipeline(ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options) error // configureConnection use the credential to set up the connection from the pipeline // to Azure configureConnection( diff --git a/cli/azd/pkg/commands/pipeline/pipeline_manager.go b/cli/azd/pkg/commands/pipeline/pipeline_manager.go index 97937f38644..c96bf91e1cc 100644 --- a/cli/azd/pkg/commands/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/commands/pipeline/pipeline_manager.go @@ -32,6 +32,7 @@ type PipelineManager struct { PipelineServicePrincipalName string PipelineRemoteName string PipelineRoleName string + PipelineProvider string Environment *environment.Environment } @@ -247,22 +248,19 @@ func (manager *PipelineManager) Configure(ctx context.Context) error { } // config pipeline handles setting or creating the provider pipeline to be used - err = manager.CiProvider.configurePipeline(ctx, gitRepoInfo) + err = manager.CiProvider.configurePipeline(ctx, gitRepoInfo, prj.Infra) if err != nil { return err } - var doPush bool = true - if existingSP == "" { - // The CI pipeline should be set-up and ready at this point. - // azd offers to push changes to the scm to start a new pipeline run - doPush, err = inputConsole.Confirm(ctx, input.ConsoleOptions{ - Message: "Would you like to commit and push your local changes to start the configured CI pipeline?", - DefaultValue: true, - }) - if err != nil { - return fmt.Errorf("prompting to push: %w", err) - } + // The CI pipeline should be set-up and ready at this point. + // azd offers to push changes to the scm to start a new pipeline run + doPush, err := inputConsole.Confirm(ctx, input.ConsoleOptions{ + Message: "Would you like to commit and push your local changes to start the configured CI pipeline?", + DefaultValue: true, + }) + if err != nil { + return fmt.Errorf("prompting to push: %w", err) } currentBranch, err := git.NewGitCli(ctx).GetCurrentBranch(ctx, manager.AzdCtx.ProjectDirectory()) diff --git a/templates/common/.azdo/pipelines/README.md b/templates/common/.azdo/pipelines/README.md index e42eed047e2..c27b8884cf3 100644 --- a/templates/common/.azdo/pipelines/README.md +++ b/templates/common/.azdo/pipelines/README.md @@ -1,71 +1,11 @@ # Azure DevOps Pipeline Configuration -This document will show you how to configure an Azure DevOps pipeline that uses the Azure Developer CLI. This logic will eventually be integrated into the `azd pipeline config` command. +This document will show you how to configure an Azure DevOps pipeline that uses the Azure Developer CLI. This can be configured by running the `azd pipeline config --provider azdo` command. You will find a default Azure DevOps pipeline file in `./.azdo/pipelines/azure-dev.yml`. It will provision your Azure resources and deploy your code upon pushes and pull requests. You are welcome to use that file as-is or modify it to suit your needs. -The following doc has many `bash` commands that need to be executed to get this setup. You can do them one-by-one, or you can [**scroll to the bottom of this page**](#script) to copy and paste them all. - -## Set Azure Developer CLI Environment Variables - -Your pipeline will need to know which Azure Developer environment and location you are targeting. Update the values below and run the following commands to set those values to be used later in this doc. - -```bash -export AZURE_ENV_NAME= -export AZURE_LOCATION= -``` - -> AZURE_ENV_NAME: This is the name of the environment you want your pipeline to work with. You can find this in the .azure folder. - -> AZURE_LOCATION: This is the location you used to deploy your azure developer environment to. You can find this in the .env file in the .azure folder for your environment. - -## Set AZURE Environment Variables - -Your pipeline will also need to know what subscription you are targeting. Let's configure those environment variables now. - -1. Run the following command to get the values needed: - -```bash -az account show -``` - -2. Copy and paste those values after the '=' and run those commands: - -```bash -export AZURE_SUBSCRIPTION_NAME= -export AZURE_SUBSCRIPTION_ID= -export AZURE_TENANT_ID= -``` - -## Create or Use Existing Service Principal - -You'll need to create or use an existing Service Principal to manipulate Azure resources from the Azure DevOps pipeline. - -If you have an existing Service Principal you'd like to use then you can skip creating one now and just set those environment variable values in the next step. - -### Create Service Principal - -1. Run the following command to create a new service principal - -```bash -az ad sp create-for-rbac --role Contributor --scopes /subscriptions/${AZURE_SUBSCRIPTION_ID} -``` - -### Set Service Principal Environment Variables - -1. Using the output from the `create-for-rbac` command above or your existing service principal, copy the values to the following environment variables and execute the commands to set them. - -```bash -export AZURE_SERVICE_PRINCIPAL_ID= -export AZURE_DEVOPS_EXT_AZURE_RM_SERVICE_PRINCIPAL_KEY= -``` - -> AZURE_SERVICE_PRINCIPAL_ID: The ID of the service principal you just created or existing one that you want to use. It will be the `appId` that is returned from the `create-for-rbac` command. - -> AZURE_DEVOPS_EXT_AZURE_RM_SERVICE_PRINCIPAL_KEY: The service principal password. It will be the `password` that is returned from the `create-for-rbac` command. - ## Create or Use Existing Azure DevOps Organization To run a pipeline in Azure DevOps, you'll first need an Azure DevOps organization. You must create an organization using the Azure DevOps portal here: https://dev.azure.com. @@ -73,256 +13,33 @@ To run a pipeline in Azure DevOps, you'll first need an Azure DevOps organizatio Once you have that organization, copy and paste it below, then run the commands to set those environment variables. ```bash -export AZURE_DEVOPS_ORG_NAME= -export AZURE_DEVOPS_ORG_URI=https://dev.azure.com/${AZURE_DEVOPS_ORG_NAME} -``` - -> AZURE_DEVOPS_ORG_NAME: The name of the Azure DevOps organization that you just created or existing one that you want to use. - -## Create or Use an Existing Azure DevOps Project - -To run a pipeline in Azure DevOps, you're organization will need a project and a repo. - -First, set the environment variable with the name of the project you want to create or use: - -> You don't have to change this value if you want to use the same name as your env name. - -```bash -export AZURE_DEVOPS_PROJECT_NAME=${AZURE_ENV_NAME} -``` - -> AZURE_DEVOPS_PROJECT_NAME: The name of the Azure DevOps project that you want to create or use an existing one. - -Then, if you want to create a new project, you can do so with the following command: - -```bash -az devops project create --name ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} -``` - -## Create or Use an Existing Azure DevOps Repo - -If you created the Project with the CLI command above then it has also created a new repo with the same name as your project. You can use that repo for this pipeline, or you can select a different one. - -If you want to use the repo that was created, then run this command. - -```bash -export AZURE_DEVOPS_REPO_NAME=${AZURE_DEVOPS_PROJECT_NAME} -``` - -If you want to use a different repo, then input that repo name below and run that command: - -```bash -export AZURE_DEVOPS_REPO_NAME= -``` - -> AZURE_DEVOPS_REPO_NAME: The name of the Azure DevOps repo that you want to use, if you don't want to use the default. - -NOTE: You can get repo info using the following command: - -```bash -az repos show --repository ${AZURE_DEVOPS_REPO_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} -``` - -## Push Code to Repo - -Now that we have the Azure DevOps org, project, and repo setup, we can push the code to that repo. - -Let's get the remote url into an environment variable: - -```bash -export BRANCH_NAME=main -export AZURE_REPO_REMOTE_URL=$(az repos show --repository ${AZURE_DEVOPS_REPO_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query remoteUrl -o tsv) -``` - -Initialize the current directory as a git repo (if not already done): - -> This script assumes your default branch is 'main'. You can set your default branch with this command: `git config --global init.defaultBranch main` and see what your current default branch is with this command: `git config --list`. - -```bash -git init -git checkout -b ${BRANCH_NAME} -git add . -git commit -m "init" -``` - -Then add that as a remote: - -```bash -git remote add ${AZURE_DEVOPS_REPO_NAME} ${AZURE_REPO_REMOTE_URL} -``` - -Then push the code: - -```bash -git push -u ${AZURE_DEVOPS_REPO_NAME} ${BRANCH_NAME} -``` - -> You may get prompted to enter a password to Azure DevOps here. Go to https://dev.azure.com and get a personal access token and then paste it into the console. - -## Create Azure DevOps Service Connection - -Azure DevOps uses a ["Service Connection"](https://docs.microsoft.com/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml) to communicate with Azure from an Azure DevOps pipeline. - -First set the connection name by running the following command: - -```bash -export AZURE_DEVOPS_SERVICE_CONNECTION_NAME=azconnection -``` - -> If you change `AZURE_DEVOPS_SERVICE_CONNECTION_NAME` to something other than `azconnection`, then you also need to update the `azureSubscription` value in `./.azdo/pipelines/azure-dev.yml` to match that new name. - -Now, let's create the service connection: - -```bash -az devops service-endpoint azurerm create --azure-rm-service-principal-id ${AZURE_SERVICE_PRINCIPAL_ID} --azure-rm-subscription-id ${AZURE_SUBSCRIPTION_ID} --azure-rm-subscription-name ${AZURE_SUBSCRIPTION_NAME} --azure-rm-tenant-id ${AZURE_TENANT_ID} --name ${AZURE_DEVOPS_SERVICE_CONNECTION_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} -``` - -If you didn't set the `AZURE_DEVOPS_EXT_AZURE_RM_SERVICE_PRINCIPAL_KEY` environment variable to your Service Principal's password, then you'll be prompted to enter that password. - -Next, we need to update the service connection to allow connections from any pipeline. We can do so with the following command: - -```bash -export AZURE_DEVOPS_SERVICE_CONNECTION_ID=$(az devops service-endpoint list --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query "[?contains(name, '${AZURE_DEVOPS_SERVICE_CONNECTION_NAME}')].id" -o tsv) -az devops service-endpoint update --id ${AZURE_DEVOPS_SERVICE_CONNECTION_ID} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --enable-for-all true -``` - -## Create an Azure DevOps Pipeline - -Now we are ready to create the pipeline in Azure DevOps. - -Let's first set the pipeline name environment variable. You can change this value if you'd like to. - -```bash -export AZURE_DEVOPS_PIPELINE_NAME=azdpipeline +export AZURE_DEVOPS_ORG_NAME="" ``` -Now let's create the pipeline by running the following command: +This can also be set as an Azure Developer CLI environment via the command: ```bash -az pipelines create --name ${AZURE_DEVOPS_PIPELINE_NAME} --branch ${BRANCH_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --repository ${AZURE_DEVOPS_REPO_NAME} --repository-type tfsgit --yml-path "./.azdo/pipelines/azure-dev.yml" --service-connection ${AZURE_DEVOPS_SERVICE_CONNECTION_NAME} --skip-first-run -``` - -## Set Pipeline Environment Variables - -Run the following commands to set the environment variables in the Azure DevOps pipeline needed to run the pipeline and target the appropriate environment. - -```bash -az pipelines variable create --name AZURE_SUBSCRIPTION_ID --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_SUBSCRIPTION_ID} --allow-override true -az pipelines variable create --name AZURE_LOCATION --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_LOCATION} --allow-override true -az pipelines variable create --name AZURE_ENV_NAME --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_ENV_NAME} --allow-override true -az pipeline variable create --name ARM_TENANT_ID --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_TENANT_ID} --secret -az pipeline variable create --name ARM_CLIENT_ID --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_SERVICE_PRINCIPAL_ID} --secret -az pipeline variable create --name ARM_CLIENT_SECRET --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_DEVOPS_EXT_AZURE_RM_SERVICE_PRINCIPAL_KEY} --secret +azd env set AZURE_DEVOPS_ORG_NAME "" ``` +> AZURE_DEVOPS_ORG_NAME: The name of the Azure DevOps organization that you just created or existing one that you want to use. -## Create Azure DevOps Build Policy +## Create a Personal Access Token -If you would like the pipeline to run every time a PR is created, then you'll need to create a build policy to do so. +The Azure Developer CLI relies on an Azure DevOps Personal Access Token (PAT) to configure an Azure DevOps project. The Azure Developer CLI will prompt you to create a PAT and provide [documentation on the PAT creation process](https://aka.ms/azure-dev/azdo-pat). -Run these commands to set the environment variables needed for the next command: ```bash -export AZURE_DEVOPS_REPO_ID=$(az repos show --repository ${AZURE_DEVOPS_REPO_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query id -o tsv) -export AZURE_DEVOPS_PIPELINE_BUILD_DEFINITION_ID=$(az pipelines build definition show --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --name ${AZURE_DEVOPS_PIPELINE_NAME} --query id) +export AZURE_DEVOPS_EXT_PAT= ``` +> AZURE_DEVOPS_EXT_PAT: The Azure DevOps Personal Access Token that you just created or existing one that you want to use. -Run this command to create the policy: +## Invoke the Pipeline configure command -```bash -az repos policy build create --blocking true --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --branch ${BRANCH_NAME} --repository-id ${AZURE_DEVOPS_REPO_ID} --enabled true --build-definition-id ${AZURE_DEVOPS_PIPELINE_BUILD_DEFINITION_ID} --queue-on-source-update-only false --manual-queue-only false --display-name "PR Build Policy" --valid-duration 0 -``` +By running `azd pipeline config --provider azdo` you can instruct the Azure Developer CLI to configure an Azure DevOps Project and Repository with a deployment Pipeline. ## Conclusion -That is everything you need to have in place to get the Azure DevOps pipeline running. You can verify that it is working by going to the Azure DevOps portal (https://dev.azure.com) and finding the pipeline you just created. - -## Script - -You can copy and paste the following into a text editor so you can easily fill in the env var values. - -```bash - -# AZURE DEV ENV VARS - # Get the following values from .azure folder - export AZURE_ENV_NAME= - export AZURE_LOCATION= - -# AZURE ENV VARS - # Run this: - az account show - # Put values here: - export AZURE_SUBSCRIPTION_NAME= - export AZURE_SUBSCRIPTION_ID= - export AZURE_TENANT_ID= - -# SERVICE PRINCIPAL - # Run this to create SP - az ad sp create-for-rbac --role Contributor --scopes /subscriptions/${AZURE_SUBSCRIPTION_ID} - - # Save the above output just in case you need it later - - # Put values here: - export AZURE_SERVICE_PRINCIPAL_ID= - export AZURE_DEVOPS_EXT_AZURE_RM_SERVICE_PRINCIPAL_KEY= +That is everything you need to have in place to get the Azure DevOps pipeline running. You can verify that it is working by going to the Azure DevOps portal (https://dev.azure.com) and finding the project you just created. -# AZURE DEVOPS ORG - export AZURE_DEVOPS_ORG_NAME= - # You don't need to update the following line: - export AZURE_DEVOPS_ORG_URI=https://dev.azure.com/${AZURE_DEVOPS_ORG_NAME} -# AZURE DEVOPS PROJECT - # You can change the AZURE_DEVOPS_PROJECT_NAME value if you don't want it to be the same as your env name - export AZURE_DEVOPS_PROJECT_NAME=${AZURE_ENV_NAME} - - az devops project create --name ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} - -# AZURE DEVOPS REPO - export AZURE_DEVOPS_REPO_NAME=${AZURE_DEVOPS_PROJECT_NAME} - - # Use following if you change the repo name to something other than the project name - # export AZURE_DEVOPS_REPO_NAME= - -# PUSH CODE - # Change branch name if necessary - export BRANCH_NAME=main - export AZURE_REPO_REMOTE_URL=$(az repos show --repository ${AZURE_DEVOPS_REPO_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query remoteUrl -o tsv) - - git init - git checkout -b ${BRANCH_NAME} - git add . - git commit -m "init" - git remote add ${AZURE_DEVOPS_REPO_NAME} ${AZURE_REPO_REMOTE_URL} - git push -u ${AZURE_DEVOPS_REPO_NAME} ${BRANCH_NAME} - - # You may get prompted to enter a password to Azure DevOps here. Go to https://dev.azure.com and get a personal access token and then paste it into the console. - -# AZURE DEVOPS SERVICE CONNECTION - export AZURE_DEVOPS_SERVICE_CONNECTION_NAME=azconnection - - az devops service-endpoint azurerm create --azure-rm-service-principal-id ${AZURE_SERVICE_PRINCIPAL_ID} --azure-rm-subscription-id ${AZURE_SUBSCRIPTION_ID} --azure-rm-subscription-name ${AZURE_SUBSCRIPTION_NAME} --azure-rm-tenant-id ${AZURE_TENANT_ID} --name ${AZURE_DEVOPS_SERVICE_CONNECTION_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} - -# AZURE DEVOPS SERVICE CONNECTION PERMISSIONS - export AZURE_DEVOPS_SERVICE_CONNECTION_ID=$(az devops service-endpoint list --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query "[?contains(name, '${AZURE_DEVOPS_SERVICE_CONNECTION_NAME}')].id" -o tsv) - - az devops service-endpoint update --id ${AZURE_DEVOPS_SERVICE_CONNECTION_ID} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --enable-for-all true - -# AZURE DEVOPS PIPELINE - export AZURE_DEVOPS_PIPELINE_NAME=azdpipeline - - az pipelines create --name ${AZURE_DEVOPS_PIPELINE_NAME} --branch ${BRANCH_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --repository ${AZURE_DEVOPS_REPO_NAME} --repository-type tfsgit --yml-path "./.azdo/pipelines/azure-dev.yml" --service-connection ${AZURE_DEVOPS_SERVICE_CONNECTION_NAME} --skip-first-run - -# AZURE DEVOPS PIPELINE VARIABLES - az pipelines variable create --name AZURE_SUBSCRIPTION_ID --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_SUBSCRIPTION_ID} --allow-override true - az pipelines variable create --name AZURE_LOCATION --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_LOCATION} --allow-override true - az pipelines variable create --name AZURE_ENV_NAME --org ${AZURE_DEVOPS_ORG_URI} --pipeline-name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --value ${AZURE_ENV_NAME} --allow-override true - -# AZURE DEVOPS BUILD POLICY - export AZURE_DEVOPS_REPO_ID=$(az repos show --repository ${AZURE_DEVOPS_REPO_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --query id -o tsv) - export AZURE_DEVOPS_PIPELINE_BUILD_DEFINITION_ID=$(az pipelines build definition show --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --name ${AZURE_DEVOPS_PIPELINE_NAME} --query id) - - az repos policy build create --blocking true --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} --branch ${BRANCH_NAME} --repository-id ${AZURE_DEVOPS_REPO_ID} --enabled true --build-definition-id ${AZURE_DEVOPS_PIPELINE_BUILD_DEFINITION_ID} --queue-on-source-update-only false --manual-queue-only false --display-name "PR Build Policy" --valid-duration 0 - -# RUN AZURE DEVOPS PIPELINE - az pipelines run --name ${AZURE_DEVOPS_PIPELINE_NAME} --project ${AZURE_DEVOPS_PROJECT_NAME} --org ${AZURE_DEVOPS_ORG_URI} -``` From 8d939852fe96efab067720eee3729f96820d94ea Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Tue, 20 Sep 2022 18:35:18 -0700 Subject: [PATCH 31/33] save pipeline provider election to env --- cli/azd/cmd/pipeline.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 49cc4c42369..9d796603797 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -12,6 +12,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/commands" "github.com/azure/azure-dev/cli/azd/pkg/commands/pipeline" + "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/spf13/cobra" @@ -108,6 +109,7 @@ func (p *pipelineConfigAction) Run( } else if overrideProvider == "github" { p.manager.ScmProvider = &pipeline.GitHubScmProvider{} p.manager.CiProvider = &pipeline.GitHubCiProvider{} + savePipelineProviderToEnv(ctx, "github", env) } else if overrideProvider == "azdo" { p.manager.ScmProvider = &pipeline.AzdoHubScmProvider{ Env: env, @@ -116,8 +118,9 @@ func (p *pipelineConfigAction) Run( Env: env, AzdContext: azdCtx, } + savePipelineProviderToEnv(ctx, "azdo", env) } else { - return fmt.Errorf("Unknown pipeline provider: %s. Supported providers: [GitHub, Azdo]", overrideProvider) + return fmt.Errorf("unknown pipeline provider: %s. Supported providers: [GitHub, Azdo]", overrideProvider) } // set context for manager @@ -126,3 +129,8 @@ func (p *pipelineConfigAction) Run( return p.manager.Configure(ctx) } + +func savePipelineProviderToEnv(ctx context.Context, provider string, env *environment.Environment) { + env.Values["PIPELINE_PROVIDER"] = provider + env.Save() +} From 22da58ef7331c1ff4f311973e0fa6c627b69e38e Mon Sep 17 00:00:00 2001 From: Hattan Shobokshi Date: Wed, 21 Sep 2022 09:06:13 -0700 Subject: [PATCH 32/33] add err check to savePipelineProviderToEnv --- cli/azd/cmd/pipeline.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 9d796603797..01b0fc52d3d 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -109,7 +109,10 @@ func (p *pipelineConfigAction) Run( } else if overrideProvider == "github" { p.manager.ScmProvider = &pipeline.GitHubScmProvider{} p.manager.CiProvider = &pipeline.GitHubCiProvider{} - savePipelineProviderToEnv(ctx, "github", env) + err := savePipelineProviderToEnv(ctx, "github", env) + if err != nil { + return nil + } } else if overrideProvider == "azdo" { p.manager.ScmProvider = &pipeline.AzdoHubScmProvider{ Env: env, @@ -118,7 +121,10 @@ func (p *pipelineConfigAction) Run( Env: env, AzdContext: azdCtx, } - savePipelineProviderToEnv(ctx, "azdo", env) + err := savePipelineProviderToEnv(ctx, "azdo", env) + if err != nil { + return err + } } else { return fmt.Errorf("unknown pipeline provider: %s. Supported providers: [GitHub, Azdo]", overrideProvider) } @@ -130,7 +136,12 @@ func (p *pipelineConfigAction) Run( return p.manager.Configure(ctx) } -func savePipelineProviderToEnv(ctx context.Context, provider string, env *environment.Environment) { +func savePipelineProviderToEnv(ctx context.Context, provider string, env *environment.Environment) error { env.Values["PIPELINE_PROVIDER"] = provider - env.Save() + err := env.Save() + if err != nil { + return err + } + + return nil } From 861a68d79dab0a05672346c3bf7c95ca8b2c8cd5 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 21 Sep 2022 16:41:36 -0700 Subject: [PATCH 33/33] Apply suggestions from code review Co-authored-by: Jon Gallant <2163001+jongio@users.noreply.github.com> --- cli/azd/cmd/pipeline.go | 2 +- cli/azd/pkg/azdo/project.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/pipeline.go b/cli/azd/cmd/pipeline.go index 01b0fc52d3d..d1998035edf 100644 --- a/cli/azd/cmd/pipeline.go +++ b/cli/azd/cmd/pipeline.go @@ -126,7 +126,7 @@ func (p *pipelineConfigAction) Run( return err } } else { - return fmt.Errorf("unknown pipeline provider: %s. Supported providers: [GitHub, Azdo]", overrideProvider) + return fmt.Errorf("unknown pipeline provider: %s. Supported providers: [github, azdo]", overrideProvider) } // set context for manager diff --git a/cli/azd/pkg/azdo/project.go b/cli/azd/pkg/azdo/project.go index 56d6140cfe0..c5824fc7330 100644 --- a/cli/azd/pkg/azdo/project.go +++ b/cli/azd/pkg/azdo/project.go @@ -105,7 +105,7 @@ func GetAzdoProjectFromNew(ctx context.Context, repoPath string, connection *azu for { name, err := console.Prompt(ctx, input.ConsoleOptions{ - Message: "Enter the name for your new Azure Devops Project OR Hit enter to use this name:", + Message: "Enter the name for your new Azure DevOps Project OR Hit enter to use this name:", DefaultValue: currentFolderName, }) if err != nil {