diff --git a/cli/azd/pkg/azapi/resource_service.go b/cli/azd/pkg/azapi/resource_service.go index 9a9dde8fd64..e3a6fd2262a 100644 --- a/cli/azd/pkg/azapi/resource_service.go +++ b/cli/azd/pkg/azapi/resource_service.go @@ -2,8 +2,10 @@ package azapi import ( "context" + "errors" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/azure/azure-dev/cli/azd/pkg/account" @@ -194,6 +196,11 @@ func (rs *ResourceService) DeleteResourceGroup(ctx context.Context, subscription } poller, err := client.BeginDelete(ctx, resourceGroupName, nil) + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { // Resource group is already deleted + return nil + } + if err != nil { return fmt.Errorf("beginning resource group deletion: %w", err) } diff --git a/cli/azd/pkg/azapi/standard_deployments.go b/cli/azd/pkg/azapi/standard_deployments.go index 9e7e1608878..f37dfbd5ee1 100644 --- a/cli/azd/pkg/azapi/standard_deployments.go +++ b/cli/azd/pkg/azapi/standard_deployments.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "net/url" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -55,6 +56,7 @@ func NewStandardDeployments( // unix time to the environment name (separated by a hyphen) to provide a unique name for each deployment. If the resulting // name is longer than the ARM limit, the longest suffix of the name under the limit is returned. func (ds *StandardDeployments) GenerateDeploymentName(baseName string) string { + log.Printf("Generating deployment name %s", baseName) name := fmt.Sprintf("%s-%d", baseName, ds.clock.Now().Unix()) if len(name) <= cArmDeploymentNameLengthMax { return name diff --git a/cli/azd/pkg/azure/tags.go b/cli/azd/pkg/azure/tags.go index 96eceb3993a..9a541973373 100644 --- a/cli/azd/pkg/azure/tags.go +++ b/cli/azd/pkg/azure/tags.go @@ -7,6 +7,11 @@ const ( // TagKeyAzdEnvName is the name of the key in the tags map of a resource // used to store the azd environment a resource is associated with. TagKeyAzdEnvName = "azd-env-name" + + // TagKeyAzdModuleName is the name of the key in the tags map of a resource + // used to store the Bicep module a resource is associated with. + TagKeyAzdModuleName = "azd-module-name" + /* #nosec G101 - Potential hardcoded credentials - false positive */ // TagKeyAzdDeploymentStateParamHashName is the name of the key in the tags map of a deployment // used to store the parameters hash. diff --git a/cli/azd/pkg/devcenter/provision_provider.go b/cli/azd/pkg/devcenter/provision_provider.go index 7ae15bdeaac..bd180a90fde 100644 --- a/cli/azd/pkg/devcenter/provision_provider.go +++ b/cli/azd/pkg/devcenter/provision_provider.go @@ -475,7 +475,7 @@ func (p *ProvisionProvider) pollForProgress(ctx context.Context, deployment infr } // Report incremental progress - progressDisplay := p.deploymentManager.ProgressDisplay(deployment) + progressDisplay := p.deploymentManager.ProgressDisplay([]infra.Deployment{deployment}) initialDelay := 3 * time.Second regularDelay := 10 * time.Second diff --git a/cli/azd/pkg/infra/deployment_manager.go b/cli/azd/pkg/infra/deployment_manager.go index a6311669370..4e401858ac4 100644 --- a/cli/azd/pkg/infra/deployment_manager.go +++ b/cli/azd/pkg/infra/deployment_manager.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "slices" "strings" @@ -47,7 +48,7 @@ func (dm *DeploymentManager) CalculateTemplateHash( return dm.deploymentService.CalculateTemplateHash(ctx, subscriptionId, template) } -func (dm *DeploymentManager) ProgressDisplay(deployment Deployment) *ProvisioningProgressDisplay { +func (dm *DeploymentManager) ProgressDisplay(deployment []Deployment) *ProvisioningProgressDisplay { return NewProvisioningProgressDisplay(dm.resourceManager, dm.console, deployment) } @@ -77,6 +78,7 @@ func (dm *DeploymentManager) CompletedDeployments( ctx context.Context, scope Scope, envName string, + moduleName string, hint string, ) ([]*azapi.ResourceDeployment, error) { deployments, err := scope.ListDeployments(ctx) @@ -106,8 +108,22 @@ func (dm *DeploymentManager) CompletedDeployments( continue } - // Match on current azd strategy (tags) or old azd strategy (deployment name) - if v, has := deployment.Tags[azure.TagKeyAzdEnvName]; has && *v == envName || deployment.Name == envName { + // Match on current azd strategy (tags) + if v, has := deployment.Tags[azure.TagKeyAzdEnvName]; has && *v == envName { + moduleVal, hasModuleTag := deployment.Tags[azure.TagKeyAzdModuleName] + if moduleName != "" && hasModuleTag && *moduleVal == moduleName { + log.Printf( + "completedDeployments: matched deployment '%s' using moduleName: %s", deployment.Name, moduleName) + return []*azapi.ResourceDeployment{deployment}, nil + } + + if moduleName == "" && (!hasModuleTag || moduleVal == nil || *moduleVal == "") { + return []*azapi.ResourceDeployment{deployment}, nil + } + } + + // LEGACY: match on deployment name + if deployment.Name == envName { return []*azapi.ResourceDeployment{deployment}, nil } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index d8a7d04491a..c4dd8fa0f66 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -23,6 +23,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/azure/azure-dev/cli/azd/pkg/account" "github.com/azure/azure-dev/cli/azd/pkg/async" "github.com/azure/azure-dev/cli/azd/pkg/azapi" @@ -116,60 +117,62 @@ var ErrEnsureEnvPreReqBicepCompileFailed = errors.New("") // EnsureEnv ensures that the environment is in a provision-ready state with required values set, prompting the user if // values are unset. This also requires that the Bicep module can be compiled. func (p *BicepProvider) EnsureEnv(ctx context.Context) error { - modulePath := p.modulePath() + modulePaths := p.modulePaths() - // for .bicepparam, we first prompt for environment values before calling compiling bicepparam file - // which can reference these values - if isBicepParamFile(modulePath) { - if err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, nil); err != nil { - return err + for _, modulePath := range modulePaths { + // for .bicepparam, we first prompt for environment values before calling compiling bicepparam file + // which can reference these values + if isBicepParamFile(modulePath) { + if err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, nil); err != nil { + return err + } } - } - compileResult, compileErr := p.compileBicep(ctx, modulePath) - if compileErr != nil { - return fmt.Errorf("%w%w", ErrEnsureEnvPreReqBicepCompileFailed, compileErr) - } + compileResult, compileErr := p.compileBicep(ctx, modulePath) + if compileErr != nil { + return fmt.Errorf("%w%w", ErrEnsureEnvPreReqBicepCompileFailed, compileErr) + } - // for .bicep, azd must load a parameters.json file and create the ArmParameters - if isBicepFile(modulePath) { - var filterLocation = func(loc account.Location) bool { - if locationParam, defined := compileResult.Template.Parameters["location"]; defined { - if locationParam.AllowedValues != nil { - return slices.IndexFunc(*locationParam.AllowedValues, func(allowedValue any) bool { - allowedValueString, goodCast := allowedValue.(string) - return goodCast && loc.Name == allowedValueString - }) != -1 + // for .bicep, azd must load a parameters.json file and create the ArmParameters + if isBicepFile(modulePath) { + var filterLocation = func(loc account.Location) bool { + if locationParam, defined := compileResult.Template.Parameters["location"]; defined { + if locationParam.AllowedValues != nil { + return slices.IndexFunc(*locationParam.AllowedValues, func(allowedValue any) bool { + allowedValueString, goodCast := allowedValue.(string) + return goodCast && loc.Name == allowedValueString + }) != -1 + } } + return true } - return true - } - err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, filterLocation) - if err != nil { - return err + err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, filterLocation) + if err != nil { + return err + } + + if _, err := p.ensureParameters(ctx, compileResult.Template); err != nil { + return err + } } - if _, err := p.ensureParameters(ctx, compileResult.Template); err != nil { + scope, err := compileResult.Template.TargetScope() + if err != nil { return err } - } - - scope, err := compileResult.Template.TargetScope() - if err != nil { - return err - } - if scope == azure.DeploymentScopeResourceGroup { - if p.env.Getenv(environment.ResourceGroupEnvVarName) == "" { - rgName, err := p.prompters.PromptResourceGroup(ctx) - if err != nil { - return err - } + if scope == azure.DeploymentScopeResourceGroup { + if p.env.Getenv(environment.ResourceGroupEnvVarName) == "" { + rgName, err := p.prompters.PromptResourceGroup(ctx) + if err != nil { + return err + } - p.env.DotenvSet(environment.ResourceGroupEnvVarName, rgName) - if err := p.envManager.Save(ctx, p.env); err != nil { - return fmt.Errorf("saving resource group name: %w", err) + p.env.DotenvSet(environment.ResourceGroupEnvVarName, rgName) + if err := p.envManager.Save(ctx, p.env); err != nil { + return fmt.Errorf("saving resource group name: %w", err) + } } } } @@ -178,7 +181,7 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error { } func (p *BicepProvider) LastDeployment(ctx context.Context) (*azapi.ResourceDeployment, error) { - modulePath := p.modulePath() + modulePath := p.rootModulePath() compileResult, err := p.compileBicep(ctx, modulePath) if err != nil { return nil, fmt.Errorf("compiling bicep template: %w", err) @@ -189,7 +192,8 @@ func (p *BicepProvider) LastDeployment(ctx context.Context) (*azapi.ResourceDepl return nil, fmt.Errorf("computing deployment scope: %w", err) } - return p.latestDeploymentResult(ctx, scope) + moduleName := submoduleName(modulePath, p.rootModulePath()) + return p.latestDeploymentResult(ctx, scope, moduleName) } func (p *BicepProvider) State(ctx context.Context, options *provisioning.StateOptions) (*provisioning.StateResult, error) { @@ -211,103 +215,113 @@ func (p *BicepProvider) State(ctx context.Context, options *provisioning.StateOp var outputs azure.ArmTemplateOutputs var scopeErr error - modulePath := p.modulePath() - if _, err := os.Stat(modulePath); err == nil { - compileResult, err := p.compileBicep(ctx, modulePath) - if err != nil { - return nil, fmt.Errorf("compiling bicep template: %w", err) - } + modulePaths := p.modulePaths() + rootModulePath := p.rootModulePath() - scope, err = p.scopeForTemplate(compileResult.Template) - if err != nil { - return nil, fmt.Errorf("computing deployment scope: %w", err) - } + state := provisioning.State{ + Resources: []provisioning.Resource{}, + Outputs: map[string]provisioning.OutputParameter{}, + } + for _, modulePath := range modulePaths { + if _, err := os.Stat(modulePath); err == nil { + compileResult, err := p.compileBicep(ctx, modulePath) + if err != nil { + return nil, fmt.Errorf("compiling bicep template: %w", err) + } - outputs = compileResult.Template.Outputs - } else if errors.Is(err, os.ErrNotExist) { - // To support BYOI (bring your own infrastructure) - // We need to support the case where there template does not contain an `infra` folder. - scope, scopeErr = p.inferScopeFromEnv() - if scopeErr != nil { - return nil, fmt.Errorf("computing deployment scope: %w", err) - } + scope, err = p.scopeForTemplate(compileResult.Template) + if err != nil { + return nil, fmt.Errorf("computing deployment scope: %w", err) + } - outputs = azure.ArmTemplateOutputs{} - } + outputs = compileResult.Template.Outputs + } else if errors.Is(err, os.ErrNotExist) { + // To support BYOI (bring your own infrastructure) + // We need to support the case where there template does not contain an `infra` folder. + scope, scopeErr = p.inferScopeFromEnv() + if scopeErr != nil { + return nil, fmt.Errorf("computing deployment scope: %w", err) + } - spinnerMessage = "Retrieving Azure deployment" - p.console.ShowSpinner(ctx, spinnerMessage, input.Step) + outputs = azure.ArmTemplateOutputs{} + } - var deployment *azapi.ResourceDeployment + spinnerMessage = "Retrieving Azure deployment" + p.console.ShowSpinner(ctx, spinnerMessage, input.Step) - deployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), options.Hint()) - p.console.StopSpinner(ctx, "", input.StepDone) + var deployment *azapi.ResourceDeployment - if err != nil { - p.console.StopSpinner(ctx, spinnerMessage, input.StepFailed) - return nil, fmt.Errorf("retrieving deployment: %w", err) - } else { + moduleName := submoduleName(modulePath, rootModulePath) + deployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), moduleName, options.Hint()) p.console.StopSpinner(ctx, "", input.StepDone) - } - if len(deployments) > 1 { - deploymentOptions := getDeploymentOptions(deployments) + if err != nil { + p.console.StopSpinner(ctx, spinnerMessage, input.StepFailed) + return nil, fmt.Errorf("retrieving deployment: %w", err) + } else { + p.console.StopSpinner(ctx, "", input.StepDone) + } + + if len(deployments) > 1 { + deploymentOptions := getDeploymentOptions(deployments) + + p.console.Message(ctx, output.WithWarningFormat("WARNING: Multiple matching deployments were found\n")) + + promptConfig := input.ConsoleOptions{ + Message: "Select a deployment to continue:", + Options: deploymentOptions, + } - p.console.Message(ctx, output.WithWarningFormat("WARNING: Multiple matching deployments were found\n")) + selectedDeployment, err := p.console.Select(ctx, promptConfig) + if err != nil { + return nil, err + } - promptConfig := input.ConsoleOptions{ - Message: "Select a deployment to continue:", - Options: deploymentOptions, + deployment = deployments[selectedDeployment] + p.console.Message(ctx, "") + } else { + deployment = deployments[0] } - selectedDeployment, err := p.console.Select(ctx, promptConfig) + azdDeployment, err := p.createDeploymentFromArmDeployment(scope, deployment.Name) if err != nil { return nil, err } - deployment = deployments[selectedDeployment] - p.console.Message(ctx, "") - } else { - deployment = deployments[0] - } + p.console.MessageUxItem(ctx, &ux.DoneMessage{ + Message: fmt.Sprintf("Retrieving Azure deployment (%s)", output.WithHighLightFormat(deployment.Name)), + }) - azdDeployment, err := p.createDeploymentFromArmDeployment(scope, deployment.Name) - if err != nil { - return nil, err - } + for _, res := range deployment.Resources { + state.Resources = append(state.Resources, provisioning.Resource{ + Id: *res.ID, + }) + } - p.console.MessageUxItem(ctx, &ux.DoneMessage{ - Message: fmt.Sprintf("Retrieving Azure deployment (%s)", output.WithHighLightFormat(deployment.Name)), - }) + outputs := p.createOutputParameters( + outputs, + azapi.CreateDeploymentOutput(deployment.Outputs), + ) - state := provisioning.State{} - state.Resources = make([]provisioning.Resource, len(deployment.Resources)) + for key, value := range outputs { + state.Outputs[key] = value + } - for idx, res := range deployment.Resources { - state.Resources[idx] = provisioning.Resource{ - Id: *res.ID, + outputsUrl, err := azdDeployment.OutputsUrl(ctx) + if err != nil { + return nil, err } - } - state.Outputs = p.createOutputParameters( - outputs, - azapi.CreateDeploymentOutput(deployment.Outputs), - ) + p.console.Message(ctx, fmt.Sprintf( + "\nFetched Azure infrastructure deployment: %s", + output.WithHyperlink(outputsUrl, deployment.Name), + )) + } p.console.MessageUxItem(ctx, &ux.DoneMessage{ Message: fmt.Sprintf("Updated %d environment variables", len(state.Outputs)), }) - outputsUrl, err := azdDeployment.OutputsUrl(ctx) - if err != nil { - return nil, err - } - - p.console.Message(ctx, fmt.Sprintf( - "\nPopulated environment from Azure infrastructure deployment: %s", - output.WithHyperlink(outputsUrl, deployment.Name), - )) - return &provisioning.StateResult{ State: &state, }, nil @@ -342,10 +356,9 @@ func isBicepParamFile(modulePath string) bool { } // Plans the infrastructure provisioning -func (p *BicepProvider) plan(ctx context.Context) (*deploymentDetails, error) { +func (p *BicepProvider) plan(ctx context.Context, modulePath string) (*deploymentDetails, error) { p.console.ShowSpinner(ctx, "Creating a deployment plan", input.Step) - modulePath := p.modulePath() compileResult, err := p.compileBicep(ctx, modulePath) if err != nil { return nil, fmt.Errorf("creating template: %w", err) @@ -395,16 +408,32 @@ func (p *BicepProvider) deploymentFromScopeType(deploymentScopeType azure.Deploy return nil, fmt.Errorf("unsupported scope: %s", deploymentScopeType) } +// submoduleName returns a suitable name for the submodule for naming deployments. +// It returns empty if the modulePath is the root module. +func submoduleName(modulePath string, rootModulePath string) string { + rootModuleDir := filepath.Dir(rootModulePath) + moduleDir := filepath.Dir(modulePath) + if !strings.HasPrefix(moduleDir, rootModuleDir) { + panic(fmt.Sprintf("moduleDir %s does not start with rootModuleDir %s", moduleDir, rootModuleDir)) + } + + subModulePath := moduleDir[len(rootModuleDir):] + subModulePath = strings.TrimPrefix(subModulePath, string(os.PathSeparator)) + name := strings.ReplaceAll(subModulePath, string(os.PathSeparator), "-") + log.Printf("submoduleName(%s, %s)=%s", modulePath, rootModulePath, name) + return name +} + // deploymentState returns the latests deployment if it is the same as the deployment within deploymentData or an error // otherwise. func (p *BicepProvider) deploymentState( ctx context.Context, deploymentData *deploymentDetails, + moduleName string, currentParamsHash string, ) (*azapi.ResourceDeployment, error) { - p.console.ShowSpinner(ctx, "Comparing deployment state", input.Step) - prevDeploymentResult, err := p.latestDeploymentResult(ctx, deploymentData.Target) + prevDeploymentResult, err := p.latestDeploymentResult(ctx, deploymentData.Target, moduleName) if err != nil { return nil, fmt.Errorf("deployment state error: %w", err) } @@ -416,6 +445,7 @@ func (p *BicepProvider) deploymentState( return nil, fmt.Errorf("last deployment failed.") } + log.Printf("deploymentState: calculating hash from previous deployment: %s", prevDeploymentResult.Name) templateHash, err := p.deploymentManager.CalculateTemplateHash( ctx, p.env.GetSubscriptionId(), deploymentData.CompiledBicep.RawArmTemplate, @@ -436,8 +466,9 @@ func (p *BicepProvider) deploymentState( func (p *BicepProvider) latestDeploymentResult( ctx context.Context, scope infra.Scope, + moduleName string, ) (*azapi.ResourceDeployment, error) { - deployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), "") + deployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), moduleName, "") // findCompletedDeployments returns error if no deployments are found // No need to check for empty list if err != nil { @@ -521,171 +552,245 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, logDS("Azure Deployment State is disabled by --no-state arg.") } - bicepDeploymentData, err := p.plan(ctx) - if err != nil { - return nil, err - } + rootModulePath := p.rootModulePath() + modulePaths := p.modulePaths() - deployment, err := p.convertToDeployment(bicepDeploymentData.CompiledBicep.Template) - if err != nil { - return nil, err + deployments := make([]*provisioning.Deployment, len(modulePaths)) + hash := make([]string, len(modulePaths)) + bicepData := make([]*deploymentDetails, len(modulePaths)) + skipped := make([]bool, len(modulePaths)) + + // create all deployments + for i, modulePath := range modulePaths { + bicepDeploymentData, err := p.plan(ctx, modulePath) + if err != nil { + return nil, err + } + + deployment, err := p.convertToDeployment(bicepDeploymentData.CompiledBicep.Template) + if err != nil { + return nil, err + } + + deployments[i] = deployment + bicepData[i] = bicepDeploymentData + + // parameters hash is required for doing deployment state validation check but also to set the hash + // after a successful deployment. + currentParamsHash, parametersHashErr := parametersHash( + bicepDeploymentData.CompiledBicep.Template.Parameters, bicepDeploymentData.CompiledBicep.Parameters) + if parametersHashErr != nil { + // Failing to hash parameters doesn't stop the provisioning. + // It only disables deployment state and recording parameters hash + logDS("%s", parametersHashErr.Error()) + } + hash[i] = currentParamsHash + + if !p.ignoreDeploymentState && parametersHashErr == nil { + moduleName := submoduleName(modulePath, rootModulePath) + deploymentState, err := p.deploymentState(ctx, bicepDeploymentData, moduleName, currentParamsHash) + if err == nil { + deployment.Outputs = p.createOutputParameters( + bicepDeploymentData.CompiledBicep.Template.Outputs, + azapi.CreateDeploymentOutput(deploymentState.Outputs), + ) + skipped[i] = true + log.Printf( + "Deployment %d skipped: state: %+v ; bicepOutputs: %+v; ", + i, deploymentState.Outputs, bicepDeploymentData.CompiledBicep.Template.Outputs) + + continue + } + logDS("%s", err.Error()) + } + + log.Printf( + "Creating deployment %d: bicepOutputs: %+v", + i, bicepDeploymentData.CompiledBicep.Template.Outputs) } - // parameters hash is required for doing deployment state validation check but also to set the hash - // after a successful deployment. - currentParamsHash, parametersHashErr := parametersHash( - bicepDeploymentData.CompiledBicep.Template.Parameters, bicepDeploymentData.CompiledBicep.Parameters) - if parametersHashErr != nil { - // fail to hash parameters won't stop the operation. It only disables deployment state and recording parameters hash - logDS("%s", parametersHashErr.Error()) + // Disable reporting progress if needed + var progressDisplay *infra.ProvisioningProgressDisplay + if use, err := strconv.ParseBool(os.Getenv("AZD_DEBUG_PROVISION_PROGRESS_DISABLE")); err == nil && use { + log.Println("Disabling progress reporting since AZD_DEBUG_PROVISION_PROGRESS_DISABLE was set") + } else { + targets := make([]infra.Deployment, 0, len(deployments)) + for i := range deployments { + if !skipped[i] { + targets = append(targets, bicepData[i].Target) + } + } + + if len(targets) > 0 { + progressDisplay = p.deploymentManager.ProgressDisplay(targets) + } } - if !p.ignoreDeploymentState && parametersHashErr == nil { - deploymentState, err := p.deploymentState(ctx, bicepDeploymentData, currentParamsHash) - if err == nil { - deployment.Outputs = p.createOutputParameters( - bicepDeploymentData.CompiledBicep.Template.Outputs, - azapi.CreateDeploymentOutput(deploymentState.Outputs), - ) + if progressDisplay != nil { + cancelProgress := make(chan bool) + defer func() { cancelProgress <- true }() + go func() { + // Make initial delay shorter to be more responsive in displaying initial progress + initialDelay := 3 * time.Second + regularDelay := 10 * time.Second + timer := time.NewTimer(initialDelay) + queryStartTime := time.Now() - return &provisioning.DeployResult{ - Deployment: deployment, - SkippedReason: provisioning.DeploymentStateSkipped, - }, nil - } - logDS("%s", err.Error()) - } - - cancelProgress := make(chan bool) - defer func() { cancelProgress <- true }() - go func() { - // Disable reporting progress if needed - if use, err := strconv.ParseBool(os.Getenv("AZD_DEBUG_PROVISION_PROGRESS_DISABLE")); err == nil && use { - log.Println("Disabling progress reporting since AZD_DEBUG_PROVISION_PROGRESS_DISABLE was set") - <-cancelProgress - return - } - - // Report incremental progress - progressDisplay := p.deploymentManager.ProgressDisplay(bicepDeploymentData.Target) - // Make initial delay shorter to be more responsive in displaying initial progress - initialDelay := 3 * time.Second - regularDelay := 10 * time.Second - timer := time.NewTimer(initialDelay) - queryStartTime := time.Now() - - for { - select { - case <-cancelProgress: - timer.Stop() - return - case <-timer.C: - if err := progressDisplay.ReportProgress(ctx, &queryStartTime); err != nil { - // We don't want to fail the whole deployment if a progress reporting error occurs - log.Printf("error while reporting progress: %s", err.Error()) - } + for { + select { + case <-cancelProgress: + timer.Stop() + return + case <-timer.C: + if err := progressDisplay.ReportProgress(ctx, &queryStartTime); err != nil { + // We don't want to fail the whole deployment if a progress reporting error occurs + log.Printf("error while reporting progress: %s", err.Error()) + } - timer.Reset(regularDelay) + timer.Reset(regularDelay) + } } - } - }() + }() + } - // Start the deployment p.console.ShowSpinner(ctx, "Creating/Updating resources", input.Step) + for i, deployment := range deployments { + if skipped[i] { + continue + } + bicepDeploymentData := bicepData[i] - deploymentTags := map[string]*string{ - azure.TagKeyAzdEnvName: to.Ptr(p.env.Name()), - } - if parametersHashErr == nil { - deploymentTags[azure.TagKeyAzdDeploymentStateParamHashName] = to.Ptr(currentParamsHash) + deploymentTags := map[string]*string{ + azure.TagKeyAzdEnvName: to.Ptr(p.env.Name()), + } + + moduleName := submoduleName(bicepDeploymentData.CompiledBicep.Path, rootModulePath) + if moduleName != "" { + deploymentTags[azure.TagKeyAzdModuleName] = to.Ptr(moduleName) + } + + if hash[i] != "" { + deploymentTags[azure.TagKeyAzdDeploymentStateParamHashName] = to.Ptr(hash[i]) + } + + optionsMap, err := convert.ToMap(p.options) + if err != nil { + return nil, err + } + + deployResult, err := bicepDeploymentData.Target.Deploy( + ctx, + bicepDeploymentData.CompiledBicep.RawArmTemplate, + bicepDeploymentData.CompiledBicep.Parameters, + deploymentTags, + optionsMap, + ) + if err != nil { + return nil, err + } + + log.Printf( + "Deployment %d: deployResult: %+v ; bicepOutputs: %+v", + i, deployResult, bicepDeploymentData.CompiledBicep.Template.Outputs) + deployment.Outputs = p.createOutputParameters( + bicepDeploymentData.CompiledBicep.Template.Outputs, + azapi.CreateDeploymentOutput(deployResult.Outputs), + ) + + log.Printf("Deployment %d completed, outputs: %+v", i, deployment.Outputs) } - optionsMap, err := convert.ToMap(p.options) - if err != nil { - return nil, err + result := &provisioning.DeployResult{} + combined := &provisioning.Deployment{ + Parameters: make(map[string]provisioning.InputParameter), + Outputs: make(map[string]provisioning.OutputParameter), } - deployResult, err := p.deployModule( - ctx, - bicepDeploymentData.Target, - bicepDeploymentData.CompiledBicep.RawArmTemplate, - bicepDeploymentData.CompiledBicep.Parameters, - deploymentTags, - optionsMap, - ) - if err != nil { - return nil, err + allSkipped := true + for i, deployment := range deployments { + for key, value := range deployment.Outputs { + combined.Outputs[key] = value + } + + allSkipped = allSkipped && skipped[i] } - deployment.Outputs = p.createOutputParameters( - bicepDeploymentData.CompiledBicep.Template.Outputs, - azapi.CreateDeploymentOutput(deployResult.Outputs), - ) + result.Deployment = combined + if allSkipped { + result.SkippedReason = provisioning.DeploymentStateSkipped + } - return &provisioning.DeployResult{ - Deployment: deployment, - }, nil + log.Printf("Deployment combined outputs %+v", result.Deployment.Outputs) + return result, nil } // Preview runs deploy using the what-if argument func (p *BicepProvider) Preview(ctx context.Context) (*provisioning.DeployPreviewResult, error) { - bicepDeploymentData, err := p.plan(ctx) - if err != nil { - return nil, err - } + var changes []*provisioning.DeploymentPreviewChange - p.console.ShowSpinner(ctx, "Generating infrastructure preview", input.Step) + for _, modulePath := range p.modulePaths() { + bicepDeploymentData, err := p.plan(ctx, modulePath) + if err != nil { + return nil, err + } - targetScope := bicepDeploymentData.Target - deployPreviewResult, err := targetScope.DeployPreview( - ctx, - bicepDeploymentData.CompiledBicep.RawArmTemplate, - bicepDeploymentData.CompiledBicep.Parameters, - ) - if err != nil { - return nil, err - } + p.console.ShowSpinner(ctx, "Generating infrastructure preview", input.Step) - if deployPreviewResult.Error != nil { - deploymentErr := *deployPreviewResult.Error - errDetailsList := make([]string, len(deploymentErr.Details)) - for index, errDetail := range deploymentErr.Details { - errDetailsList[index] = fmt.Sprintf( - "code: %s, message: %s", - convert.ToValueWithDefault(errDetail.Code, ""), - convert.ToValueWithDefault(errDetail.Message, ""), - ) + targetScope := bicepDeploymentData.Target + deployPreviewResult, err := targetScope.DeployPreview( + ctx, + bicepDeploymentData.CompiledBicep.RawArmTemplate, + bicepDeploymentData.CompiledBicep.Parameters, + ) + if err != nil { + return nil, err } - var errDetails string - if len(errDetailsList) > 0 { - errDetails = fmt.Sprintf(" Details: %s", strings.Join(errDetailsList, "\n")) - } - return nil, fmt.Errorf( - "generating preview: error code: %s, message: %s.%s", - convert.ToValueWithDefault(deploymentErr.Code, ""), - convert.ToValueWithDefault(deploymentErr.Message, ""), - errDetails, - ) - } + if deployPreviewResult.Error != nil { + deploymentErr := *deployPreviewResult.Error + errDetailsList := make([]string, len(deploymentErr.Details)) + for index, errDetail := range deploymentErr.Details { + errDetailsList[index] = fmt.Sprintf( + "code: %s, message: %s", + convert.ToValueWithDefault(errDetail.Code, ""), + convert.ToValueWithDefault(errDetail.Message, ""), + ) + } - var changes []*provisioning.DeploymentPreviewChange - for _, change := range deployPreviewResult.Properties.Changes { - resourceAfter := change.After.(map[string]interface{}) + var errDetails string + if len(errDetailsList) > 0 { + errDetails = fmt.Sprintf(" Details: %s", strings.Join(errDetailsList, "\n")) + } + return nil, fmt.Errorf( + "generating preview: error code: %s, message: %s.%s", + convert.ToValueWithDefault(deploymentErr.Code, ""), + convert.ToValueWithDefault(deploymentErr.Message, ""), + errDetails, + ) + } - changes = append(changes, &provisioning.DeploymentPreviewChange{ - ChangeType: provisioning.ChangeType(*change.ChangeType), - ResourceId: provisioning.Resource{ - Id: *change.ResourceID, - }, - ResourceType: resourceAfter["type"].(string), - Name: resourceAfter["name"].(string), - }) + for _, change := range deployPreviewResult.Properties.Changes { + resourceAfter := change.After.(map[string]interface{}) + + if !slices.ContainsFunc(changes, func(c *provisioning.DeploymentPreviewChange) bool { + return c.ChangeType == provisioning.ChangeType(*change.ChangeType) && + c.ResourceId.Id == *change.ResourceID + }) { + changes = append(changes, &provisioning.DeploymentPreviewChange{ + ChangeType: provisioning.ChangeType(*change.ChangeType), + ResourceId: provisioning.Resource{ + Id: *change.ResourceID, + }, + ResourceType: resourceAfter["type"].(string), + Name: resourceAfter["name"].(string), + }) + } + } } return &provisioning.DeployPreviewResult{ Preview: &provisioning.DeploymentPreview{ - Status: *deployPreviewResult.Status, + Status: "done", Properties: &provisioning.DeploymentPreviewProperties{ Changes: changes, }, @@ -731,43 +836,67 @@ func (p *BicepProvider) Destroy( ctx context.Context, options provisioning.DestroyOptions, ) (*provisioning.DestroyResult, error) { - modulePath := p.modulePath() - p.console.ShowSpinner(ctx, "Discovering resources to delete...", input.Step) - defer p.console.StopSpinner(ctx, "", input.StepDone) - compileResult, err := p.compileBicep(ctx, modulePath) - if err != nil { - return nil, fmt.Errorf("creating template: %w", err) - } + modulePaths := p.modulePaths() + rootModulePath := p.rootModulePath() + invalidatedEnvKeys := map[string]bool{} + + deploymentsToDelete := make([]infra.Deployment, 0, len(modulePaths)) + allResourcesToDelete := []*armresources.ResourceReference{} + compileResults := make([]*compileBicepResult, 0, len(modulePaths)) + mostRecentDeployments := make([]*azapi.ResourceDeployment, 0, len(modulePaths)) + scopes := make([]infra.Scope, 0, len(modulePaths)) + for _, modulePath := range modulePaths { + p.console.ShowSpinner(ctx, "Discovering resources to delete...", input.Step) + defer p.console.StopSpinner(ctx, "", input.StepDone) + compileResult, err := p.compileBicep(ctx, modulePath) + if err != nil { + return nil, fmt.Errorf("creating template: %w", err) + } - scope, err := p.scopeForTemplate(compileResult.Template) - if err != nil { - return nil, fmt.Errorf("computing deployment scope: %w", err) - } + scope, err := p.scopeForTemplate(compileResult.Template) + if err != nil { + return nil, fmt.Errorf("computing deployment scope: %w", err) + } - completedDeployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), "") - if err != nil { - return nil, fmt.Errorf("finding completed deployments: %w", err) - } + subModuleName := submoduleName(modulePath, rootModulePath) + completedDeployments, err := p.deploymentManager.CompletedDeployments(ctx, scope, p.env.Name(), subModuleName, "") + if err != nil { + return nil, fmt.Errorf("finding completed deployments: %w", err) + } - if len(completedDeployments) == 0 { - return nil, fmt.Errorf("no deployments found for environment, '%s'", p.env.Name()) - } + if len(completedDeployments) == 0 { + return nil, fmt.Errorf("no deployments found for environment, '%s'", p.env.Name()) + } - mostRecentDeployment := completedDeployments[0] - deploymentToDelete := scope.Deployment(mostRecentDeployment.Name) + mostRecentDeployment := completedDeployments[0] + deploymentToDelete := scope.Deployment(mostRecentDeployment.Name) - resourcesToDelete, err := deploymentToDelete.Resources(ctx) - if err != nil { - return nil, fmt.Errorf("getting resources to delete: %w", err) + resourcesToDelete, err := deploymentToDelete.Resources(ctx) + if err != nil { + return nil, fmt.Errorf("getting resources to delete: %w", err) + } + + allResourcesToDelete = append(allResourcesToDelete, resourcesToDelete...) + deploymentsToDelete = append(deploymentsToDelete, deploymentToDelete) + compileResults = append(compileResults, compileResult) + mostRecentDeployments = append(mostRecentDeployments, mostRecentDeployment) + scopes = append(scopes, scope) } - groupedResources, err := azapi.GroupByResourceGroup(resourcesToDelete) + groupedResources, err := azapi.GroupByResourceGroup(allResourcesToDelete) if err != nil { return nil, fmt.Errorf("mapping resources to resource groups: %w", err) } if len(groupedResources) == 0 { - return nil, fmt.Errorf("%w, '%s'", infra.ErrDeploymentResourcesNotFound, deploymentToDelete.Name()) + deploymentNames := make([]string, len(deploymentsToDelete)) + for i, deployment := range deploymentsToDelete { + deploymentNames[i] = deployment.Name() + } + return nil, fmt.Errorf( + "%w, '%s'", + infra.ErrDeploymentResourcesNotFound, + strings.Join(deploymentNames, ", ")) } keyVaults, err := p.getKeyVaultsToPurge(ctx, groupedResources) @@ -796,12 +925,13 @@ func (p *BicepProvider) Destroy( } p.console.StopSpinner(ctx, "", input.StepDone) + if err := p.destroyDeploymentWithConfirmation( ctx, options, - deploymentToDelete, + deploymentsToDelete, groupedResources, - len(resourcesToDelete), + len(allResourcesToDelete), ); err != nil { return nil, fmt.Errorf("deleting resource groups: %w", err) } @@ -860,21 +990,24 @@ func (p *BicepProvider) Destroy( return nil, fmt.Errorf("purging resources: %w", err) } - destroyResult := &provisioning.DestroyResult{ - InvalidatedEnvKeys: slices.Collect(maps.Keys(p.createOutputParameters( - compileResult.Template.Outputs, - azapi.CreateDeploymentOutput(mostRecentDeployment.Outputs), - ))), - } + for i := range deploymentsToDelete { + envKeysInvalidated := p.createOutputParameters( + compileResults[i].Template.Outputs, + azapi.CreateDeploymentOutput(mostRecentDeployments[i].Outputs)) + // Since we have deleted the resource group, add AZURE_RESOURCE_GROUP to the list of invalidated env vars + // so it will be removed from the .env file. + if _, ok := scopes[i].(*infra.ResourceGroupScope); ok { + envKeysInvalidated[environment.ResourceGroupEnvVarName] = provisioning.OutputParameter{} + } - // Since we have deleted the resource group, add AZURE_RESOURCE_GROUP to the list of invalidated env vars - // so it will be removed from the .env file. - if _, ok := scope.(*infra.ResourceGroupScope); ok { - destroyResult.InvalidatedEnvKeys = append( - destroyResult.InvalidatedEnvKeys, environment.ResourceGroupEnvVarName, - ) + for key := range envKeysInvalidated { + invalidatedEnvKeys[key] = true + } } + destroyResult := &provisioning.DestroyResult{ + InvalidatedEnvKeys: slices.Collect(maps.Keys(invalidatedEnvKeys)), + } return destroyResult, nil } @@ -1003,7 +1136,7 @@ func (p *BicepProvider) generateResourcesToDelete(groupedResources map[string][] func (p *BicepProvider) destroyDeploymentWithConfirmation( ctx context.Context, options provisioning.DestroyOptions, - deployment infra.Deployment, + deployments []infra.Deployment, groupedResources map[string][]*azapi.Resource, resourceCount int, ) error { @@ -1041,12 +1174,18 @@ func (p *BicepProvider) destroyDeploymentWithConfirmation( p.console.StopSpinner(ctx, progressMessage.Message, input.StepFailed) } }, func(progress *async.Progress[azapi.DeleteDeploymentProgress]) error { - optionsMap, err := convert.ToMap(p.options) - if err != nil { - return err - } + for _, deployment := range deployments { + optionsMap, err := convert.ToMap(p.options) + if err != nil { + return err + } - return deployment.Delete(ctx, optionsMap, progress) + err = deployment.Delete(ctx, optionsMap, progress) + if err != nil { + return err + } + } + return nil }) if err != nil { @@ -1549,6 +1688,7 @@ func (p *BicepProvider) compileBicep( ctx context.Context, modulePath string, ) (*compileBicepResult, error) { if val, ok := p.compileBicepMemoryCache[modulePath]; ok { + log.Printf("return cached bicep module at %s, outputs: %s", modulePath, val.Template.Outputs) return &val, nil } @@ -1658,6 +1798,7 @@ func (p *BicepProvider) compileBicep( } p.compileBicepMemoryCache[modulePath] = result + log.Printf("compiled bicep module at %s, outputs: %s", modulePath, result.Template.Outputs) return &result, nil } @@ -1721,21 +1862,9 @@ func (p *BicepProvider) convertToDeployment(bicepTemplate azure.ArmTemplate) (*p return &template, nil } -// Deploys the specified Bicep module and parameters with the selected provisioning scope (subscription vs resource group) -func (p *BicepProvider) deployModule( - ctx context.Context, - target infra.Deployment, - armTemplate azure.RawArmTemplate, - armParameters azure.ArmParameters, - tags map[string]*string, - options map[string]any, -) (*azapi.ResourceDeployment, error) { - return target.Deploy(ctx, armTemplate, armParameters, tags, options) -} - // Returns either the bicep or bicepparam module file located in the infrastructure root. // The bicepparam file is preferred over bicep file. -func (p *BicepProvider) modulePath() string { +func (p *BicepProvider) rootModulePath() string { infraRoot := p.options.Path moduleName := p.options.Module @@ -1755,6 +1884,34 @@ func (p *BicepProvider) modulePath() string { return filepath.Join(infraRoot, moduleFilename) } +func (p *BicepProvider) azdModulePath() string { + // if p.alpha is enabled + infraRoot := p.options.Path + if !filepath.IsAbs(infraRoot) { + infraRoot = filepath.Join(p.projectPath, infraRoot) + } + + azdInfra := filepath.Join(infraRoot, "azd", "main.bicep") + + if _, err := os.Stat(azdInfra); err == nil { + return azdInfra + } + + return "" +} + +func (p *BicepProvider) modulePaths() []string { + result := []string{ + p.rootModulePath(), + } + + if azdModulePath := p.azdModulePath(); azdModulePath != "" { + result = append(result, azdModulePath) + } + + return result +} + // inputsParameter generates and updates input parameters for the Azure Resource Manager (ARM) template. // It takes an existingInputs map that contains the current input values for each resource, and an autoGenParameters map // that contains information about the input parameters to be generated. @@ -2077,16 +2234,17 @@ func NewBicepProvider( cloud *cloud.Cloud, ) provisioning.Provider { return &BicepProvider{ - envManager: envManager, - env: env, - console: console, - azCli: azCli, - bicepCli: bicepCli, - resourceService: resourceService, - deploymentManager: deploymentManager, - prompters: prompters, - curPrincipal: curPrincipal, - keyvaultService: keyvaultService, - portalUrlBase: cloud.PortalUrlBase, + envManager: envManager, + env: env, + console: console, + azCli: azCli, + bicepCli: bicepCli, + resourceService: resourceService, + deploymentManager: deploymentManager, + prompters: prompters, + curPrincipal: curPrincipal, + keyvaultService: keyvaultService, + portalUrlBase: cloud.PortalUrlBase, + compileBicepMemoryCache: make(map[string]compileBicepResult), } } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go index a4b74931d72..2394cfb4b59 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go @@ -48,7 +48,7 @@ func TestBicepPlan(t *testing.T) { prepareBicepMocks(mockContext) infraProvider := createBicepProvider(t, mockContext) - deploymentPlan, err := infraProvider.plan(*mockContext.Context) + deploymentPlan, err := infraProvider.plan(*mockContext.Context, infraProvider.rootModulePath()) require.Nil(t, err) @@ -104,7 +104,7 @@ func TestBicepPlanPrompt(t *testing.T) { }).Respond(false) infraProvider := createBicepProvider(t, mockContext) - plan, err := infraProvider.plan(*mockContext.Context) + plan, err := infraProvider.plan(*mockContext.Context, infraProvider.rootModulePath()) require.NoError(t, err) @@ -290,7 +290,7 @@ func TestPlanForResourceGroup(t *testing.T) { infraProvider := createBicepProvider(t, mockContext) // The computed plan should target the resource group we picked. - planResult, err := infraProvider.plan(*mockContext.Context) + planResult, err := infraProvider.plan(*mockContext.Context, infraProvider.rootModulePath()) require.Nil(t, err) require.NotNil(t, planResult) require.Equal(t, "rg-test-env", @@ -868,7 +868,7 @@ func TestFindCompletedDeployments(t *testing.T) { *mockContext.Context, &mockedScope{ baseDate: baseDate, envTag: envTag, - }, envTag, "") + }, envTag, "", "") require.NoError(t, err) require.Equal(t, 1, len(deployments)) // should take the base date + 2 years diff --git a/cli/azd/pkg/infra/provisioning_progress_display.go b/cli/azd/pkg/infra/provisioning_progress_display.go index 32727442e12..770370b3e6e 100644 --- a/cli/azd/pkg/infra/provisioning_progress_display.go +++ b/cli/azd/pkg/infra/provisioning_progress_display.go @@ -29,17 +29,17 @@ type ProvisioningProgressDisplay struct { displayedResources map[string]bool resourceManager ResourceManager console input.Console - deployment Deployment + deployments []Deployment } func NewProvisioningProgressDisplay( rm ResourceManager, console input.Console, - deployment Deployment, + deployments []Deployment, ) *ProvisioningProgressDisplay { return &ProvisioningProgressDisplay{ displayedResources: map[string]bool{}, - deployment: deployment, + deployments: deployments, resourceManager: rm, console: console, } @@ -50,7 +50,8 @@ func NewProvisioningProgressDisplay( func (display *ProvisioningProgressDisplay) ReportProgress( ctx context.Context, queryStart *time.Time) error { if !display.deploymentStarted { - _, err := display.deployment.Get(ctx) + deployment := display.deployments[0] + _, err := deployment.Get(ctx) if err != nil { // Return default progress log.Printf("error while reporting progress: %s", err.Error()) @@ -58,7 +59,7 @@ func (display *ProvisioningProgressDisplay) ReportProgress( } display.deploymentStarted = true - deploymentUrl, err := display.deployment.DeploymentUrl(ctx) + deploymentUrl, err := deployment.DeploymentUrl(ctx) if err != nil { return err } @@ -87,47 +88,50 @@ func (display *ProvisioningProgressDisplay) ReportProgress( ) } - operations, err := display.resourceManager.GetDeploymentResourceOperations(ctx, display.deployment, queryStart) - if err != nil { - // Status display is best-effort activity. - return err - } + for _, deployment := range display.deployments { + operations, err := display.resourceManager.GetDeploymentResourceOperations(ctx, deployment, queryStart) + if err != nil { + // Status display is best-effort activity. + return err + } - newlyDeployedResources := []*armresources.DeploymentOperation{} - newlyFailedResources := []*armresources.DeploymentOperation{} - runningDeployments := []*armresources.DeploymentOperation{} - - for i := range operations { - if operations[i].Properties.TargetResource != nil { - resourceId := *operations[i].Properties.TargetResource.ResourceName - - if !display.displayedResources[resourceId] { - switch *operations[i].Properties.ProvisioningState { - case string(armresources.ProvisioningStateSucceeded): - newlyDeployedResources = append(newlyDeployedResources, operations[i]) - case string(armresources.ProvisioningStateRunning): - runningDeployments = append(runningDeployments, operations[i]) - case string(armresources.ProvisioningStateFailed): - newlyFailedResources = append(newlyFailedResources, operations[i]) + newlyDeployedResources := []*armresources.DeploymentOperation{} + newlyFailedResources := []*armresources.DeploymentOperation{} + runningDeployments := []*armresources.DeploymentOperation{} + + for i := range operations { + if operations[i].Properties.TargetResource != nil { + resourceId := *operations[i].Properties.TargetResource.ResourceName + + if !display.displayedResources[resourceId] { + switch *operations[i].Properties.ProvisioningState { + case string(armresources.ProvisioningStateSucceeded): + newlyDeployedResources = append(newlyDeployedResources, operations[i]) + case string(armresources.ProvisioningStateRunning): + runningDeployments = append(runningDeployments, operations[i]) + case string(armresources.ProvisioningStateFailed): + newlyFailedResources = append(newlyFailedResources, operations[i]) + } } } } - } - sort.Slice(newlyDeployedResources, func(i int, j int) bool { - return time.Time.Before( - *newlyDeployedResources[i].Properties.Timestamp, - *newlyDeployedResources[j].Properties.Timestamp, - ) - }) + sort.Slice(newlyDeployedResources, func(i int, j int) bool { + return time.Time.Before( + *newlyDeployedResources[i].Properties.Timestamp, + *newlyDeployedResources[j].Properties.Timestamp, + ) + }) - displayedResources := append(newlyDeployedResources, newlyFailedResources...) - display.logNewlyCreatedResources(ctx, displayedResources, runningDeployments) + displayedResources := append(newlyDeployedResources, newlyFailedResources...) + display.logNewlyCreatedResources(ctx, deployment, displayedResources, runningDeployments) + } return nil } func (display *ProvisioningProgressDisplay) logNewlyCreatedResources( ctx context.Context, + deployment Deployment, resources []*armresources.DeploymentOperation, inProgressResources []*armresources.DeploymentOperation, ) { @@ -135,7 +139,7 @@ func (display *ProvisioningProgressDisplay) logNewlyCreatedResources( resourceTypeName := *resource.Properties.TargetResource.ResourceType resourceTypeDisplayName, err := display.resourceManager.GetResourceTypeDisplayName( ctx, - display.deployment.SubscriptionId(), + deployment.SubscriptionId(), *resource.Properties.TargetResource.ID, azapi.AzureResourceType(resourceTypeName), ) @@ -180,7 +184,7 @@ func (display *ProvisioningProgressDisplay) logNewlyCreatedResources( resourceTypeName := *inProgResource.Properties.TargetResource.ResourceType resourceTypeDisplayName, err := display.resourceManager.GetResourceTypeDisplayName( ctx, - display.deployment.SubscriptionId(), + deployment.SubscriptionId(), *inProgResource.Properties.TargetResource.ID, azapi.AzureResourceType(resourceTypeName), ) diff --git a/cli/azd/pkg/infra/provisioning_progress_display_test.go b/cli/azd/pkg/infra/provisioning_progress_display_test.go index f097210eeab..19136c03db5 100644 --- a/cli/azd/pkg/infra/provisioning_progress_display_test.go +++ b/cli/azd/pkg/infra/provisioning_progress_display_test.go @@ -139,7 +139,7 @@ func TestReportProgress(t *testing.T) { startTime := time.Now() outputLength := 0 mockResourceManager := mockResourceManager{} - progressDisplay := NewProvisioningProgressDisplay(&mockResourceManager, mockContext.Console, deployment) + progressDisplay := NewProvisioningProgressDisplay(&mockResourceManager, mockContext.Console, []Deployment{deployment}) err := progressDisplay.ReportProgress(*mockContext.Context, &startTime) require.NoError(t, err)