diff --git a/cli/azd/pkg/azapi/resource_service.go b/cli/azd/pkg/azapi/resource_service.go index aadfdb88e2d..1afcfef3d66 100644 --- a/cli/azd/pkg/azapi/resource_service.go +++ b/cli/azd/pkg/azapi/resource_service.go @@ -96,6 +96,21 @@ func (rs *ResourceService) GetResource( }, nil } +func (rs *ResourceService) CheckExistenceByID( + ctx context.Context, resourceId arm.ResourceID, apiVersion string) (bool, error) { + client, err := rs.createResourcesClient(ctx, resourceId.SubscriptionID) + if err != nil { + return false, err + } + + response, err := client.CheckExistenceByID(ctx, resourceId.String(), apiVersion, nil) + if err != nil { + return false, fmt.Errorf("checking resource existence by id: %w", err) + } + + return response.Success, nil +} + func (rs *ResourceService) GetRawResource( ctx context.Context, resourceId arm.ResourceID, apiVersion string) (string, error) { client, err := rs.createResourcesClient(ctx, resourceId.SubscriptionID) diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 5d9c01ee0d1..c461d00d84c 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -588,8 +588,29 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, } if !p.ignoreDeploymentState && parametersHashErr == nil { - deploymentState, err := p.deploymentState(ctx, planned, deployment, currentParamsHash) - if err == nil { + deploymentState, stateErr := p.deploymentState(ctx, planned, deployment, currentParamsHash) + if stateErr == nil { + // As a heuristic, we also check the existence of all resource groups + // created by the deployment to validate the deployment state. + // This handles the scenario of resource group(s) being deleted outside of azd, + // which is quite common. + // This check adds ~100ms per resource group to the deployment time. + for _, res := range deploymentState.Resources { + if res != nil && res.ID != nil { + resId, err := arm.ParseResourceID(*res.ID) + if err == nil && resId.ResourceType.Type == arm.ResourceGroupResourceType.Type { + exists, err := p.resourceService.CheckExistenceByID(ctx, *resId, "2025-03-01") + if err == nil && !exists { + stateErr = fmt.Errorf( + "resource group %s no longer exists, invalidating deployment state", resId.ResourceGroupName) + break + } + } + } + } + } + + if stateErr == nil { result.Outputs = provisioning.OutputParametersFromArmOutputs( planned.Template.Outputs, azapi.CreateDeploymentOutput(deploymentState.Outputs), @@ -600,7 +621,7 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, SkippedReason: provisioning.DeploymentStateSkipped, }, nil } - logDS("%s", err.Error()) + logDS("%s", stateErr.Error()) } deploymentTags := map[string]*string{ diff --git a/cli/azd/test/recording/recording.go b/cli/azd/test/recording/recording.go index 067698dfa4f..da091b264ea 100644 --- a/cli/azd/test/recording/recording.go +++ b/cli/azd/test/recording/recording.go @@ -344,7 +344,7 @@ func Start(t *testing.T, opts ...Options) *Session { return } - t.Fatal("recorderProxy: " + msg) + t.Log("recorderProxy: " + msg) }, Recorder: vcr, },