func()

in cli/azd/pkg/pipeline/pipeline_manager.go [227:542]


func (pm *PipelineManager) Configure(ctx context.Context, projectName string) (result *PipelineConfigResult, err error) {
	// check all required tools are installed
	requiredTools, err := pm.requiredTools(ctx)
	if err != nil {
		return result, err
	}
	if err := tools.EnsureInstalled(ctx, requiredTools...); err != nil {
		return result, err
	}

	userConfig, err := pm.userConfigManager.Load()
	if err != nil {
		return result, fmt.Errorf("loading user configuration: %w", err)
	}
	smr := resolveSmr(pm.args.ServiceManagementReference, pm.env.Config, userConfig)
	if smr != nil {
		if _, err := uuid.Parse(*smr); err != nil {
			return result, fmt.Errorf("Invalid service management reference %s: %w", *smr, err)
		}
	}

	infra := pm.infra
	// run pre-config validations.
	rootPath := pm.azdCtx.ProjectDirectory()
	updatedConfig, errorsFromPreConfig := pm.preConfigureCheck(ctx, infra.Options, rootPath)
	if errorsFromPreConfig != nil {
		return result, errorsFromPreConfig
	}
	if updatedConfig {
		pm.console.Message(ctx, "")
	}

	// Get git repo details
	gitRepoInfo, err := pm.getGitRepoDetails(ctx)
	if err != nil {
		return result, fmt.Errorf("ensuring git remote: %w", err)
	}

	if pm.args.PipelineServicePrincipalName != "" && pm.args.PipelineServicePrincipalId != "" {
		//nolint:lll
		return result, fmt.Errorf(
			"you have specified both --principal-id and --principal-name, but only one of these parameters should be used at a time.",
		)
	}

	// see if SP already exists - This step will not create the SP if it doesn't exist.
	spConfig, err := servicePrincipal(
		ctx, pm.env.Getenv(AzurePipelineClientIdEnvVarName), pm.env.GetSubscriptionId(), pm.args, pm.entraIdService)
	if err != nil {
		return result, err
	}

	// Update the message depending on the SP already exists or not
	var displayMsg string
	if spConfig.servicePrincipal == nil {
		displayMsg = fmt.Sprintf("Creating service principal %s", spConfig.applicationName)
	} else {
		displayMsg = fmt.Sprintf("Updating service principal %s (%s)",
			spConfig.servicePrincipal.DisplayName,
			spConfig.servicePrincipal.AppId)
	}

	pm.console.ShowSpinner(ctx, displayMsg, input.Step)
	description := fmt.Sprintf("Created by Azure Developer CLI for project: %s", projectName)
	options := entraid.CreateOrUpdateServicePrincipalOptions{
		RolesToAssign:              pm.args.PipelineRoleNames,
		Description:                &description,
		ServiceManagementReference: smr,
	}
	servicePrincipal, err := pm.entraIdService.CreateOrUpdateServicePrincipal(
		ctx,
		pm.env.GetSubscriptionId(),
		spConfig.appIdOrName,
		options)

	if err != nil {
		pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err))
		return result, fmt.Errorf("failed to create or update service principal: %w", err)
	}

	if !strings.Contains(displayMsg, servicePrincipal.AppId) {
		displayMsg += fmt.Sprintf(" (%s)", servicePrincipal.AppId)
	}
	pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err))

	// Set in .env to be retrieved for any additional runs
	pm.env.DotenvSet(AzurePipelineClientIdEnvVarName, servicePrincipal.AppId)
	if err := pm.envManager.Save(ctx, pm.env); err != nil {
		return result, fmt.Errorf("failed to save environment: %w", err)
	}

	repoSlug := gitRepoInfo.owner + "/" + gitRepoInfo.repoName
	displayMsg = fmt.Sprintf("Configuring repository %s to use credentials for %s", repoSlug, spConfig.applicationName)
	pm.console.ShowSpinner(ctx, displayMsg, input.Step)

	subscriptionId := pm.env.GetSubscriptionId()
	credentials := &entraid.AzureCredentials{
		ClientId:       servicePrincipal.AppId,
		TenantId:       *servicePrincipal.AppOwnerOrganizationId,
		SubscriptionId: subscriptionId,
	}

	// Get the requested credential options from the CI provider
	credentialOptions, err := pm.ciProvider.credentialOptions(
		ctx,
		gitRepoInfo,
		infra.Options,
		PipelineAuthType(pm.args.PipelineAuthTypeName),
		credentials,
	)
	if err != nil {
		return result, fmt.Errorf("failed to get credential options: %w", err)
	}

	// Enable client credentials if requested
	if credentialOptions.EnableClientCredentials {
		spinnerMessage := "Configuring client credentials for service principal"
		pm.console.ShowSpinner(ctx, spinnerMessage, input.Step)

		creds, err := pm.entraIdService.ResetPasswordCredentials(ctx, subscriptionId, servicePrincipal.AppId)
		pm.console.StopSpinner(ctx, spinnerMessage, input.GetStepResultFormat(err))
		if err != nil {
			return result, fmt.Errorf("failed to reset password credentials: %w", err)
		}

		credentials = creds
	}

	// Enable federated credentials if requested
	if credentialOptions.EnableFederatedCredentials {
		createdCredentials, err := pm.entraIdService.ApplyFederatedCredentials(
			ctx, subscriptionId,
			servicePrincipal.AppId,
			credentialOptions.FederatedCredentialOptions,
		)
		if err != nil {
			return result, fmt.Errorf("failed to create federated credentials: %w", err)
		}

		for _, credential := range createdCredentials {
			pm.console.MessageUxItem(
				ctx,
				&ux.DisplayedResource{
					Type: fmt.Sprintf("Federated identity credential for %s", pm.ciProvider.Name()),
					Name: fmt.Sprintf("subject %s", credential.Subject),
				},
			)
		}
	}

	err = pm.ciProvider.configureConnection(
		ctx,
		gitRepoInfo,
		infra.Options,
		servicePrincipal,
		credentialOptions,
		credentials,
	)

	pm.console.StopSpinner(ctx, "", input.GetStepResultFormat(err))
	if err != nil {
		return result, err
	}

	// Adding environment.AzdInitialEnvironmentConfigName as a secret to the pipeline as the base configuration for
	// whenever a new environment is created. This means loading the local environment config into a pipeline secret which
	// azd will use to restore the the config on CI
	localEnvConfig, err := json.Marshal(pm.env.Config.ResolvedRaw())
	if err != nil {
		return result, fmt.Errorf("failed to marshal environment config: %w", err)
	}

	defaultAzdSecrets := map[string]string{
		environment.AzdInitialEnvironmentConfigName: string(localEnvConfig),
	}

	defaultAzdVariables := map[string]string{}
	// If the user has set the resource group name as an environment variable, we need to pass it to the pipeline
	// as this likely means rg-deployment
	if rgGroup, exists := pm.env.LookupEnv(environment.ResourceGroupEnvVarName); exists {
		defaultAzdVariables[environment.ResourceGroupEnvVarName] = rgGroup
	}

	// Merge azd default variables and secrets with the ones defined on azure.yaml
	pm.configOptions.variables, pm.configOptions.secrets = mergeProjectVariablesAndSecrets(
		pm.configOptions.projectVariables, pm.configOptions.projectSecrets,
		defaultAzdVariables, defaultAzdSecrets, pm.env.Dotenv())

	// resolve akvs secrets
	// For each akvs in the secrets array:
	// azd gets the value from Azure Key Vault and use it as a secret in the pipeline
	for key, value := range pm.configOptions.secrets {
		if !strings.HasPrefix(value, "akvs://") {
			continue
		}
		kvSecret, err := pm.keyVaultService.SecretFromAkvs(ctx, value)
		if err != nil {
			return result, fmt.Errorf("failed to resolve akvs '%s': %w", key, err)
		}
		pm.configOptions.secrets[key] = kvSecret
	}
	// For each akvs in the variables array:
	// azd must grant read access role to the pipelines's identity to read the akvs
	displayMsg = "Assigning read access role for Key Vault to service principal"
	pm.console.ShowSpinner(ctx, displayMsg, input.Step)
	kvAccounts := make(map[string]struct{})
	for key, value := range pm.configOptions.variables {
		if !strings.HasPrefix(value, "akvs://") {
			continue
		}

		akvs, err := keyvault.ParseAzureKeyVaultSecret(value)
		if err != nil {
			return result, fmt.Errorf("failed to parse akvs '%s': %w", key, err)
		}
		kvId := akvs.SubscriptionId + akvs.VaultName
		if _, ok := kvAccounts[kvId]; ok {
			// skip if already assigned role for this key vault
			continue
		}

		// can't use keyvaultService.Get() because it requires the resource group name and we don't save it for akvs
		allKvFromSub, err := pm.keyVaultService.ListSubscriptionVaults(ctx, akvs.SubscriptionId)
		if err != nil {
			return result, fmt.Errorf(
				"assigning read access role for Key Vault to service principal: %w", err)
		}
		var vaultResourceId string
		foundKeyVault := slices.ContainsFunc(allKvFromSub, func(kv keyvault.Vault) bool {
			if kv.Name == akvs.VaultName {
				vaultResourceId = kv.Id
				return true
			}
			return false
		})
		if !foundKeyVault {
			return result, fmt.Errorf(
				"assigning read access role for Key Vault to service principal: "+
					"key vault '%s' not found in subscription '%s'", akvs.VaultName, akvs.SubscriptionId)
		}

		// CreateRbac uses the azure-sdk RoleAssignmentsClient.Create() which creates or updates the role assignment
		// We don't need to check if the role assignment already exists, the method will handle it.
		err = pm.entraIdService.CreateRbac(
			ctx, akvs.SubscriptionId, vaultResourceId, keyvault.RoleIdKeyVaultSecretsUser, *servicePrincipal.Id)
		if err != nil {
			return result, fmt.Errorf(
				"assigning read access role for Key Vault to service principal: %w", err)
		}
		// save the kvId to avoid assigning the role multiple times for the same key vault
		kvAccounts[kvId] = struct{}{}
	}
	pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err))

	// config pipeline handles setting or creating the provider pipeline to be used
	ciPipeline, err := pm.ciProvider.configurePipeline(ctx, gitRepoInfo, pm.configOptions)
	if err != nil {
		return result, 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 := pm.console.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 result, fmt.Errorf("prompting to push: %w", err)
	}

	// scm provider can prevent from pushing changes and/or use the
	// interactive console for setting up any missing details.
	// For example, GitHub provider would check if GH-actions are disabled.
	if doPush {
		preventPush, err := pm.scmProvider.preventGitPush(
			ctx,
			gitRepoInfo,
			pm.args.PipelineRemoteName,
			gitRepoInfo.branch)
		if err != nil {
			return result, fmt.Errorf("check git push prevent: %w", err)
		}
		// revert user's choice when prevent git push returns true
		doPush = !preventPush
	}

	if doPush {
		err = pm.pushGitRepo(ctx, gitRepoInfo, gitRepoInfo.branch)
		if err != nil {
			return result, fmt.Errorf("git push: %w", err)
		}

		// The spinner can't run during `pushing changes` the next UX messages are purely simulated
		displayMsg := "Pushing changes"
		pm.console.Message(ctx, "") // new line before the step
		pm.console.ShowSpinner(ctx, displayMsg, input.Step)
		pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err))

		displayMsg = "Queuing pipeline"
		pm.console.ShowSpinner(ctx, displayMsg, input.Step)
		gitRepoInfo.pushStatus = true
		pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err))
	} else {
		pm.console.Message(ctx,
			fmt.Sprintf(
				"To fully enable pipeline you need to push this repo to the upstream "+
					"using 'git push --set-upstream %s %s'.\n",
				pm.args.PipelineRemoteName,
				gitRepoInfo.branch))
	}

	return &PipelineConfigResult{
		RepositoryLink: gitRepoInfo.url,
		PipelineLink:   ciPipeline.url(),
	}, nil
}