cli/azd/pkg/pipeline/azdo_provider.go (683 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package pipeline
import (
"context"
"errors"
"fmt"
"log"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/azure/azure-dev/cli/azd/pkg/azdo"
"github.com/azure/azure-dev/cli/azd/pkg/entraid"
"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/exec"
"github.com/azure/azure-dev/cli/azd/pkg/graphsdk"
"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/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"
azdoGit "github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
)
// AzdoScmProvider implements ScmProvider using Azure DevOps as the provider
// for source control manager.
type AzdoScmProvider struct {
envManager environment.Manager
repoDetails *AzdoRepositoryDetails
env *environment.Environment
azdContext *azdcontext.AzdContext
azdoConnection *azuredevops.Connection
commandRunner exec.CommandRunner
console input.Console
gitCli *git.Cli
}
func NewAzdoScmProvider(
envManager environment.Manager,
env *environment.Environment,
azdContext *azdcontext.AzdContext,
commandRunner exec.CommandRunner,
console input.Console,
gitCli *git.Cli,
) ScmProvider {
return &AzdoScmProvider{
envManager: envManager,
env: env,
azdContext: azdContext,
commandRunner: commandRunner,
console: console,
gitCli: gitCli,
}
}
// 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
repoId string
orgName string
repoName string
repoWebUrl string
remoteUrl string
sshUrl string
buildDefinition *build.BuildDefinition
}
// *** subareaProvider implementation ******
// requiredTools return the list of external tools required by
// Azure DevOps provider during its execution.
func (p *AzdoScmProvider) requiredTools(_ context.Context) ([]tools.ExternalTool, error) {
return []tools.ExternalTool{}, nil
}
// preConfigureCheck check the current state of external tools and any
// other dependency to be as expected for execution.
func (p *AzdoScmProvider) preConfigureCheck(
ctx context.Context,
pipelineManagerArgs PipelineManagerArgs,
infraOptions provisioning.Options,
projectPath string,
) (bool, error) {
_, updatedPat, err := azdo.EnsurePatExists(ctx, p.env, p.console)
if err != nil {
return updatedPat, err
}
_, updatedOrg, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.env, p.console)
return (updatedPat || updatedOrg), err
}
// helper function to save configuration values to .env file
func (p *AzdoScmProvider) saveEnvironmentConfig(ctx context.Context, key string, value string) error {
p.env.DotenvSet(key, value)
err := p.envManager.Save(ctx, p.env)
if err != nil {
return err
}
return nil
}
// name returns the name of the provider
func (p *AzdoScmProvider) Name() string {
return azdoDisplayName
}
// *** scmProvider implementation ******
// stores repo details in state for use in other functions. Also saves AzDo project details to .env
func (p *AzdoScmProvider) 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(ctx, azdo.AzDoEnvironmentRepoIdName, p.repoDetails.repoId)
if err != nil {
return fmt.Errorf("error saving repo id to environment %w", err)
}
err = p.saveEnvironmentConfig(ctx, azdo.AzDoEnvironmentRepoName, p.repoDetails.repoName)
if err != nil {
return fmt.Errorf("error saving repo name to environment %w", err)
}
err = p.saveEnvironmentConfig(ctx, azdo.AzDoEnvironmentRepoWebUrl, p.repoDetails.repoWebUrl)
if err != nil {
return fmt.Errorf("error saving repo web url to environment %w", err)
}
return nil
}
// prompts the user for a new AzDo Git repo and creates the repo
func (p *AzdoScmProvider) createNewGitRepositoryFromInput(ctx context.Context, console input.Console) (string, error) {
connection, err := p.getAzdoConnection(ctx)
if err != nil {
return "", err
}
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 := azdo.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://aka.ms/azure-dev/azdo-repo-naming\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
}
// verifies that a repo exists or prompts the user to select from a list of existing AzDo repos
func (p *AzdoScmProvider) 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 := azdo.GetGitRepositoriesInProject(ctx, p.repoDetails.projectName, p.repoDetails.orgName, connection, console)
if err != nil {
return "", err
}
err = p.StoreRepoDetails(ctx, repo)
if err != nil {
return "", err
}
return *repo.RemoteUrl, nil
}
// helper function to return repoDetails from state
func (p *AzdoScmProvider) getRepoDetails() *AzdoRepositoryDetails {
if p.repoDetails != nil {
return p.repoDetails
}
repoDetails := &AzdoRepositoryDetails{}
p.repoDetails = repoDetails
return p.repoDetails
}
// helper function to return an azuredevops.Connection for use with AzDo Go SDK
func (p *AzdoScmProvider) getAzdoConnection(ctx context.Context) (*azuredevops.Connection, error) {
if p.azdoConnection != nil {
return p.azdoConnection, nil
}
org, _, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.env, p.console)
if err != nil {
return nil, err
}
repoDetails := p.getRepoDetails()
repoDetails.orgName = org
pat, _, err := azdo.EnsurePatExists(ctx, p.env, p.console)
if err != nil {
return nil, err
}
connection, err := azdo.GetConnection(ctx, org, pat)
if err != nil {
return nil, err
}
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 *AzdoScmProvider) 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
}
idx, err := console.Select(ctx, input.ConsoleOptions{
Message: "How would you like to configure your git remote to Azure DevOps?",
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 "", "", false, fmt.Errorf("prompting for azdo project type: %w", err)
}
connection, err := p.getAzdoConnection(ctx)
if err != nil {
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 = azdo.GetProjectFromExisting(ctx, connection, console)
if err != nil {
return "", "", false, err
}
// Create a new AzDo project
case 1:
projectName, projectId, err = azdo.GetProjectFromNew(
ctx,
p.azdContext.ProjectDirectory(),
connection,
p.env,
console,
)
newProject = true
if err != nil {
return "", "", false, err
}
default:
panic(fmt.Sprintf("unexpected selection index %d", idx))
}
return projectName, projectId, newProject, nil
}
// configureGitRemote set up or create the git project and git remote
func (p *AzdoScmProvider) configureGitRemote(
ctx context.Context,
repoPath string,
remoteName string,
) (string, error) {
projectName, projectId, newProject, err := p.ensureProjectExists(ctx, p.console)
if err != nil {
return "", err
}
repoDetails := p.getRepoDetails()
repoDetails.projectName = projectName
repoDetails.projectId = projectId
err = p.saveEnvironmentConfig(ctx, azdo.AzDoEnvironmentProjectIdName, projectId)
if err != nil {
return "", fmt.Errorf("error saving project id to environment %w", err)
}
err = p.saveEnvironmentConfig(ctx, azdo.AzDoEnvironmentProjectName, projectName)
if err != nil {
return "", fmt.Errorf("error saving project name to environment %w", err)
}
var remoteUrl string
if !newProject {
remoteUrl, err = p.promptForAzdoRepository(ctx, p.console)
if err != nil {
return "", err
}
} else {
remoteUrl, err = p.getDefaultRepoRemote(ctx, projectName)
if err != nil {
return "", err
}
}
branch, err := p.getCurrentGitBranch(ctx, repoPath)
if err != nil {
return "", err
}
azdo.DefaultBranch = branch
return remoteUrl, nil
}
func (p *AzdoScmProvider) getCurrentGitBranch(ctx context.Context, repoPath string) (string, error) {
branch, err := p.gitCli.GetCurrentBranch(ctx, repoPath)
if err != nil {
return "", err
}
return branch, nil
}
// returns the git remote for a newly created repo that is part of a newly created AzDo project
func (p *AzdoScmProvider) getDefaultRepoRemote(
ctx context.Context,
projectName string,
) (string, error) {
connection, err := p.getAzdoConnection(ctx)
if err != nil {
return "", err
}
repo, err := azdo.GetDefaultGitRepositoriesInProject(ctx, projectName, connection)
if err != nil {
return "", err
}
err = p.StoreRepoDetails(ctx, repo)
if err != nil {
return "", err
}
return *repo.RemoteUrl, nil
}
// prompt the user to select azdo repo or create a new one
func (p *AzdoScmProvider) 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),
Options: []string{
"Select an existing Azure DevOps Repository",
"Create a new private Azure DevOps Repository",
},
DefaultValue: "Create a new private Azure DevOps Repository",
})
if err != nil {
return "", fmt.Errorf("prompting for remote configuration type: %w", err)
}
switch idx {
// Select from an existing Azure DevOps project
case 0:
remoteUrl, err = p.ensureGitRepositoryExists(ctx, console)
if err != nil {
return "", err
}
// Create a new project
case 1:
remoteUrl, err = p.createNewGitRepositoryFromInput(ctx, console)
if err != nil {
return "", err
}
default:
panic(fmt.Sprintf("unexpected selection index %d", idx))
}
return remoteUrl, nil
}
// ErrRemoteHostIsNotAzDo the error used when a non Azure DevOps remote is found
var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an Azure DevOps host")
// ErrSSHNotSupported the error used when ssh git remote is detected
var ErrSSHNotSupported = errors.New("ssh git remote is not supported. " +
"Use HTTPS git remote to connect the remote repository")
type azdoRemote struct {
Project string
RepositoryName string
}
// parseAzDoRemote extracts the organization, project and repository name from an Azure DevOps remote url
// the url can be in the form of:
// - https://dev.azure.com/[org|user]/[project]/_git/[repo]
// - https://[user]@dev.azure.com/[org|user]/[project]/_git/[repo]
// - https://[org].visualstudio.com/[project]/_git/[repo]
// - git@ssh.dev.azure.com:v[1-3]/[user|org]/[project]/[repo]
// - git@vs-ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
// - git@ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
func parseAzDoRemote(remoteUrl string) (*azdoRemote, error) {
// Initialize the azdoRemote struct
azdoRemote := &azdoRemote{}
if !strings.Contains(remoteUrl, "visualstudio.com") && !strings.Contains(remoteUrl, "dev.azure.com") {
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}
if strings.Contains(remoteUrl, "/_git/") {
// applies to http or https
parts := strings.Split(remoteUrl, "/_git/")
projectNameStart := strings.LastIndex(parts[0], "/")
projectPartLen := len(parts[0])
if len(parts) != 2 || // remoteUrl must have exactly one "/_git/" substring
!strings.Contains(parts[0], "/") || // part 0 (the project) must have more than one "/"
projectPartLen <= 1 || // part 0 must be greater than 1 character
projectNameStart == projectPartLen-1 { // part 0 must not end with "/"
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}
azdoRemote.Project = parts[0][projectNameStart+1:]
azdoRemote.RepositoryName = parts[1]
return azdoRemote, nil
}
if strings.Contains(remoteUrl, "git@") {
// applies to git@ -> project and repo always in the last two parts
parts := strings.Split(remoteUrl, "/")
partsLen := len(parts)
azdoRemote.Project = parts[partsLen-2]
azdoRemote.RepositoryName = parts[partsLen-1]
return azdoRemote, nil
}
// If the remoteUrl does not match any of the supported formats, return an error
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
}
// gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts
// like owner, name and path
func (p *AzdoScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) {
repoDetails := p.getRepoDetails()
// Try getting values from the env.
// This is a quick shortcut to avoid parsing the remote in detail.
// While using the same .env file, the outputs from creating a project and repository
// are memorized in .env file
if repoDetails.orgName == "" {
repoDetails.orgName = p.env.Getenv(azdo.AzDoEnvironmentOrgName)
}
if repoDetails.projectName == "" {
repoDetails.projectName = p.env.Getenv(azdo.AzDoEnvironmentProjectName)
}
if repoDetails.projectId == "" {
repoDetails.projectId = p.env.Getenv(azdo.AzDoEnvironmentProjectIdName)
}
if repoDetails.repoName == "" {
repoDetails.repoName = p.env.Getenv(azdo.AzDoEnvironmentRepoName)
}
if repoDetails.repoId == "" {
repoDetails.repoId = p.env.Getenv(azdo.AzDoEnvironmentRepoIdName)
}
if repoDetails.repoWebUrl == "" {
repoDetails.repoWebUrl = p.env.Getenv(azdo.AzDoEnvironmentRepoWebUrl)
}
if repoDetails.remoteUrl == "" {
repoDetails.remoteUrl = remoteUrl
}
if repoDetails.projectId == "" || repoDetails.repoId == "" {
// Removing environment or creating a new one would remove any memory from project
// and repo. In that case, it needs to be calculated from the remote url
azdoRemote, err := parseAzDoRemote(remoteUrl)
if err != nil {
return nil, fmt.Errorf("parsing Azure DevOps remote url: %s: %w", remoteUrl, err)
}
repoDetails.projectName = azdoRemote.Project
p.env.DotenvSet(azdo.AzDoEnvironmentProjectName, repoDetails.projectName)
repoDetails.repoName = azdoRemote.RepositoryName
p.env.DotenvSet(azdo.AzDoEnvironmentRepoName, repoDetails.repoName)
connection, err := p.getAzdoConnection(ctx)
if err != nil {
return nil, fmt.Errorf("Getting azdo connection: %w", err)
}
repo, err := azdo.GetGitRepository(ctx, repoDetails.projectName, repoDetails.repoName, connection)
if err != nil {
return nil, fmt.Errorf("Looking for repository: %w", err)
}
repoDetails.repoId = repo.Id.String()
p.env.DotenvSet(azdo.AzDoEnvironmentRepoIdName, repoDetails.repoId)
repoDetails.repoWebUrl = *repo.WebUrl
p.env.DotenvSet(azdo.AzDoEnvironmentRepoWebUrl, repoDetails.repoWebUrl)
proj, err := azdo.GetProjectByName(ctx, connection, repoDetails.projectName)
if err != nil {
return nil, fmt.Errorf("Looking for project: %w", err)
}
repoDetails.projectId = proj.Id.String()
p.env.DotenvSet(azdo.AzDoEnvironmentProjectIdName, repoDetails.projectId)
if err := p.envManager.Save(ctx, p.env); err != nil {
return nil, fmt.Errorf("saving environment: %w", err)
}
}
return &gitRepositoryDetails{
owner: p.repoDetails.orgName,
repoName: p.repoDetails.repoName,
details: repoDetails,
remote: repoDetails.remoteUrl,
url: repoDetails.repoWebUrl,
}, nil
}
// preventGitPush is nil for Azure DevOps
func (p *AzdoScmProvider) preventGitPush(
ctx context.Context,
gitRepo *gitRepositoryDetails,
remoteName string,
branchName string) (bool, error) {
return false, nil
}
func azdoPat(ctx context.Context, env *environment.Environment, console input.Console) string {
pat, _, err := azdo.EnsurePatExists(ctx, env, console)
if err != nil {
log.Printf("Error getting PAT when it should be found: %s", err.Error())
}
return pat
}
func gitInsteadOfConfig(
pat string,
gitRepo *gitRepositoryDetails) (string, string) {
azdoRepoDetails := gitRepo.details.(*AzdoRepositoryDetails)
remoteAndPatUrl := fmt.Sprintf("url.https://%s@%s/", pat, azdo.AzDoHostName)
originalUrl := fmt.Sprintf("https://%s@%s/", azdoRepoDetails.orgName, azdo.AzDoHostName)
return remoteAndPatUrl, originalUrl
}
// Push code and queue pipeline
func (p *AzdoScmProvider) GitPush(
ctx context.Context,
gitRepo *gitRepositoryDetails,
remoteName string,
branchName string) error {
// ** Push code with PAT
// This is the same as gitCli.PushUpstream(), but it adds `-c url.PAT+HostName.insteadOf=HostName` to execute
// git push with the PAT to authenticate
pat := azdoPat(ctx, p.env, p.console)
remoteAndPatUrl, originalUrl := gitInsteadOfConfig(pat, gitRepo)
runArgs := exec.NewRunArgsWithSensitiveData("git",
[]string{
"-C",
gitRepo.gitProjectPath,
"-c",
fmt.Sprintf("%s.insteadOf=%s", remoteAndPatUrl, originalUrl),
"push",
"--set-upstream",
"--quiet",
remoteName,
branchName,
},
[]string{
pat,
},
).WithInteractive(true)
if _, err := p.commandRunner.Run(ctx, runArgs); err != nil {
// this error should not fail the operation
log.Printf("Error setting git config: insteadOf url: %s", err.Error())
}
// *** Queue pipeline
connection, err := p.getAzdoConnection(ctx)
if err != nil {
return err
}
err = azdo.CreateBuildPolicy(
ctx,
connection,
p.repoDetails.projectId,
p.repoDetails.repoId,
p.repoDetails.buildDefinition,
p.env,
)
if err != nil {
return err
}
err = azdo.QueueBuild(
ctx, connection, p.repoDetails.projectId, p.repoDetails.buildDefinition, branchName)
if err != nil {
return err
}
return nil
}
// AzdoCiProvider implements a CiProvider using Azure DevOps to manage CI with azdo pipelines.
type AzdoCiProvider struct {
envManager environment.Manager
Env *environment.Environment
AzdContext *azdcontext.AzdContext
credentials *entraid.AzureCredentials
console input.Console
commandRunner exec.CommandRunner
}
func NewAzdoCiProvider(
envManager environment.Manager,
env *environment.Environment,
azdContext *azdcontext.AzdContext,
console input.Console,
commandRunner exec.CommandRunner,
) CiProvider {
return &AzdoCiProvider{
envManager: envManager,
Env: env,
AzdContext: azdContext,
console: console,
commandRunner: commandRunner,
}
}
// *** subareaProvider implementation ******
// requiredTools defines the requires tools for GitHub to be used as CI manager
func (p *AzdoCiProvider) requiredTools(_ context.Context) ([]tools.ExternalTool, error) {
return []tools.ExternalTool{}, nil
}
// preConfigureCheck nil for Azdo
func (p *AzdoCiProvider) preConfigureCheck(
ctx context.Context,
pipelineManagerArgs PipelineManagerArgs,
infraOptions provisioning.Options,
projectPath string,
) (bool, error) {
authType := PipelineAuthType(pipelineManagerArgs.PipelineAuthTypeName)
if authType == AuthTypeFederated {
return false, fmt.Errorf(
//nolint:lll
"Azure DevOps does not support federated authentication. To explicitly use client credentials set the %s flag. %w",
output.WithBackticks("--auth-type client-credentials"),
ErrAuthNotSupported,
)
}
_, updatedPat, err := azdo.EnsurePatExists(ctx, p.Env, p.console)
if err != nil {
return updatedPat, err
}
_, updatedOrg, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.Env, p.console)
return (updatedPat || updatedOrg), err
}
// name returns the name of the provider.
func (p *AzdoCiProvider) Name() string {
return azdoDisplayName
}
// *** ciProvider implementation ******
func (p *AzdoCiProvider) credentialOptions(
ctx context.Context,
repoDetails *gitRepositoryDetails,
infraOptions provisioning.Options,
authType PipelineAuthType,
credentials *entraid.AzureCredentials,
) (*CredentialOptions, error) {
// Default auth type to client-credentials for terraform
if infraOptions.Provider == provisioning.Terraform && authType == "" {
authType = AuthTypeClientCredentials
}
if authType == AuthTypeClientCredentials {
return &CredentialOptions{
EnableClientCredentials: true,
}, nil
}
// If not specified default to federated credentials
if authType == "" || authType == AuthTypeFederated {
p.credentials = credentials
details := repoDetails.details.(*AzdoRepositoryDetails)
org, _, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.Env, p.console)
if err != nil {
return nil, err
}
pat, _, err := azdo.EnsurePatExists(ctx, p.Env, p.console)
if err != nil {
return nil, err
}
connection, err := azdo.GetConnection(ctx, org, pat)
if err != nil {
return nil, err
}
sConnection, err := azdo.CreateServiceConnection(
ctx, connection, details.projectId, details.projectName, *p.Env, p.credentials, p.console)
if err != nil {
return nil, err
}
federatedCredentials := []*graphsdk.FederatedIdentityCredential{
{
Name: "AzureDevOpsOIDC", //Must not contain a space character and 3 to 64 characters in length
Issuer: (*sConnection.Authorization.Parameters)["workloadIdentityFederationIssuer"],
Subject: (*sConnection.Authorization.Parameters)["workloadIdentityFederationSubject"],
Description: to.Ptr("Created by Azure Developer CLI"),
Audiences: []string{federatedIdentityAudience},
},
}
return &CredentialOptions{
EnableFederatedCredentials: true,
FederatedCredentialOptions: federatedCredentials,
}, nil
}
return &CredentialOptions{
EnableClientCredentials: false,
EnableFederatedCredentials: false,
}, nil
}
// configureConnection set up Azure DevOps with the Azure credential
func (p *AzdoCiProvider) configureConnection(
ctx context.Context,
repoDetails *gitRepositoryDetails,
provisioningProvider provisioning.Options,
servicePrincipal *graphsdk.ServicePrincipal,
credentialOptions *CredentialOptions,
credentials *entraid.AzureCredentials,
) error {
if credentialOptions.EnableFederatedCredentials {
// default and federated credentials are set up in credentialOptions
return nil
}
p.credentials = credentials
// create service connection for client credentials
details := repoDetails.details.(*AzdoRepositoryDetails)
org, _, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.Env, p.console)
if err != nil {
return err
}
pat, _, err := azdo.EnsurePatExists(ctx, p.Env, p.console)
if err != nil {
return err
}
connection, err := azdo.GetConnection(ctx, org, pat)
if err != nil {
return err
}
_, err = azdo.CreateServiceConnection(
ctx, connection, details.projectId, details.projectName, *p.Env, p.credentials, p.console)
return err
}
// configurePipeline create Azdo pipeline
func (p *AzdoCiProvider) configurePipeline(
ctx context.Context,
repoDetails *gitRepositoryDetails,
options *configurePipelineOptions,
) (CiPipeline, error) {
details := repoDetails.details.(*AzdoRepositoryDetails)
org, _, err := azdo.EnsureOrgNameExists(ctx, p.envManager, p.Env, p.console)
if err != nil {
return nil, err
}
pat, _, err := azdo.EnsurePatExists(ctx, p.Env, p.console)
if err != nil {
return nil, err
}
connection, err := azdo.GetConnection(ctx, org, pat)
if err != nil {
return nil, err
}
buildDefinition, err := azdo.CreatePipeline(
ctx,
details.projectId,
azdo.AzurePipelineName,
details.repoName,
connection,
p.credentials,
p.Env,
p.console,
*options.provisioningProvider,
options.secrets,
options.variables,
)
if err != nil {
return nil, err
}
details.buildDefinition = buildDefinition
return &pipeline{
repoDetails: details,
}, nil
}
// pipeline is the implementation for a CiPipeline for Azure DevOps
type pipeline struct {
repoDetails *AzdoRepositoryDetails
}
func (p *pipeline) name() string {
return *p.repoDetails.buildDefinition.Name
}
func (p *pipeline) url() string {
repoUrl := p.repoDetails.repoWebUrl
repoPrefix := strings.Split(repoUrl, "_git")[0]
return fmt.Sprintf("%s_build?definitionId=%d", repoPrefix, *p.repoDetails.buildDefinition.Id)
}