Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions cli/azd/pkg/azapi/standard_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/url"
"slices"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
Expand Down Expand Up @@ -343,32 +345,17 @@ func (ds *StandardDeployments) ListSubscriptionDeploymentResources(
return nil, fmt.Errorf("getting subscription deployment: %w", err)
}

// Get the environment name from the deployment tags
envName, has := subscriptionDeployment.Tags[azure.TagKeyAzdEnvName]
if !has || envName == nil {
return nil, fmt.Errorf("environment name not found in deployment tags")
}

// Get all resource groups tagged with the azd-env-name tag
resourceGroups, err := ds.resourceService.ListResourceGroup(ctx, subscriptionId, &ListResourceGroupOptions{
TagFilter: &Filter{Key: azure.TagKeyAzdEnvName, Value: *envName},
})

if err != nil {
return nil, fmt.Errorf("listing resource groups: %w", err)
}

resourceGroupNames := resourceGroupsFromDeployment(subscriptionDeployment)
allResources := []*armresources.ResourceReference{}

// Find all the resources from all the resource groups
for _, resourceGroup := range resourceGroups {

resources, err := ds.resourceService.ListResourceGroupResources(ctx, subscriptionId, resourceGroup.Name, nil)
// Find all the resources from the deployment's resource groups
for _, resourceGroupName := range resourceGroupNames {
resources, err := ds.resourceService.ListResourceGroupResources(ctx, subscriptionId, resourceGroupName, nil)
if err != nil {
return nil, fmt.Errorf("listing resource group resources: %w", err)
}

resourceGroupId := azure.ResourceGroupRID(subscriptionId, resourceGroup.Name)
resourceGroupId := azure.ResourceGroupRID(subscriptionId, resourceGroupName)
allResources = append(allResources, &armresources.ResourceReference{
ID: &resourceGroupId,
})
Expand All @@ -383,6 +370,39 @@ func (ds *StandardDeployments) ListSubscriptionDeploymentResources(
return allResources, nil
}

// resourceGroupsFromDeployment extracts the unique resource groups associated to a deployment.
func resourceGroupsFromDeployment(deployment *ResourceDeployment) []string {
resourceGroups := map[string]struct{}{}

if deployment.ProvisioningState == DeploymentProvisioningStateSucceeded {
// For a successful deployment, use the output resources property to find the resource groups.
for _, resourceId := range deployment.Resources {
if resourceId != nil && resourceId.ID != nil {
resId, err := arm.ParseResourceID(*resourceId.ID)
if err == nil && resId.ResourceGroupName != "" {
resourceGroups[resId.ResourceGroupName] = struct{}{}
}
}
}
} else {
// For a failed deployment, the outputResources field is not populated.
// Instead, look at the dependencies to find resource groups that this deployment deployed into.
for _, dependency := range deployment.Dependencies {
if dependency.ResourceType != nil && *dependency.ResourceType == string(AzureResourceTypeDeployment) {
for _, dependent := range dependency.DependsOn {
if dependent.ResourceType != nil && *dependent.ResourceType == arm.ResourceGroupResourceType.String() {
if dependent.ResourceName != nil {
resourceGroups[*dependent.ResourceName] = struct{}{}
}
}
}
}
}
}

return slices.Collect(maps.Keys(resourceGroups))
}

func (ds *StandardDeployments) ListResourceGroupDeploymentResources(
ctx context.Context,
subscriptionId string,
Expand Down
80 changes: 80 additions & 0 deletions cli/azd/pkg/azapi/standard_deployments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ package azapi

import (
"context"
"sort"
"testing"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_StandardDeployments_GenerateDeploymentName(t *testing.T) {
Expand Down Expand Up @@ -45,3 +49,79 @@ func Test_StandardDeployments_GenerateDeploymentName(t *testing.T) {
assert.LessOrEqual(t, len(deploymentName), 64)
}
}

func TestResourceGroupsFromDeployment(t *testing.T) {
t.Parallel()

t.Run("references used when no output resources", func(t *testing.T) {
mockDeployment := &ResourceDeployment{
//nolint:lll
Id: "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
//nolint:lll
DeploymentId: "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
Name: "matell-2508",
Type: "Microsoft.Resources/deployments",
Tags: map[string]*string{
"azd-env-name": to.Ptr("matell-2508"),
},
ProvisioningState: DeploymentProvisioningStateFailed,
Timestamp: time.Now(),
Dependencies: []*armresources.Dependency{
{
//nolint:lll
ID: to.Ptr(
"/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg/providers/Microsoft.Resources/deployments/resources",
),
ResourceName: to.Ptr("resources"),
ResourceType: to.Ptr("Microsoft.Resources/deployments"),
DependsOn: []*armresources.BasicDependency{
{
//nolint:lll
ID: to.Ptr(
"/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg",
),
ResourceName: to.Ptr("matell-2508-rg"),
ResourceType: to.Ptr("Microsoft.Resources/resourceGroups"),
},
},
},
},
}

require.Equal(t, []string{"matell-2508-rg"}, resourceGroupsFromDeployment(mockDeployment))
})

t.Run("duplicate resource groups ignored", func(t *testing.T) {

mockDeployment := ResourceDeployment{
Id: "DEPLOYMENT_ID",
Name: "test-env",
Resources: []*armresources.ResourceReference{
{
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupA"),
},
{
ID: to.Ptr(
"/subscriptions/sub-id/resourceGroups/groupA/Microsoft.Storage/storageAccounts/storageAccount",
),
},
{
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupB"),
},
{
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupB/Microsoft.web/sites/test"),
},
{
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupC"),
},
},
ProvisioningState: DeploymentProvisioningStateSucceeded,
Timestamp: time.Now(),
}

groups := resourceGroupsFromDeployment(&mockDeployment)

sort.Strings(groups)
require.Equal(t, []string{"groupA", "groupB", "groupC"}, groups)
})
}
110 changes: 110 additions & 0 deletions cli/azd/pkg/azapi/testdata/failed-subscription-deployment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

{
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
"location": "eastus2",
"name": "matell-2508-1689982746",
"properties": {
"correlationId": "84c13af8bdb9709a44dda61f93cce798",
"debugSetting": null,
"dependencies": [
{
"dependsOn": [
{
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg",
"resourceName": "matell-2508-rg",
"resourceType": "Microsoft.Resources/resourceGroups"
}
],
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg/providers/Microsoft.Resources/deployments/resources",
"resourceGroup": "matell-2508-rg",
"resourceName": "resources",
"resourceType": "Microsoft.Resources/deployments"
}
],
"duration": "PT1M39.7689131S",
"error": {
"additionalInfo": null,
"code": "DeploymentFailed",
"details": [
{
"additionalInfo": null,
"code": "DeploymentFailed",
"details": null,
"message": "At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-deployment-operations for usage details.",
"target": null
}
],
"message": "At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-deployment-operations for usage details.",
"target": null
},
"mode": "Incremental",
"onErrorDeployment": null,
"outputResources": null,
"outputs": null,
"parameters": {
"databasePassword": {
"type": "SecureString"
},
"location": {
"type": "String",
"value": "eastus2"
},
"name": {
"type": "String",
"value": "matell-2508"
},
"secretKey": {
"type": "SecureString"
}
},
"parametersLink": null,
"providers": [
{
"id": null,
"namespace": "Microsoft.Resources",
"providerAuthorizationConsentState": null,
"registrationPolicy": null,
"registrationState": null,
"resourceTypes": [
{
"aliases": null,
"apiProfiles": null,
"apiVersions": null,
"capabilities": null,
"defaultApiVersion": null,
"locationMappings": null,
"locations": [
"eastus2"
],
"properties": null,
"resourceType": "resourceGroups",
"zoneMappings": null
},
{
"aliases": null,
"apiProfiles": null,
"apiVersions": null,
"capabilities": null,
"defaultApiVersion": null,
"locationMappings": null,
"locations": [
null
],
"properties": null,
"resourceType": "deployments",
"zoneMappings": null
}
]
}
],
"provisioningState": "Failed",
"templateHash": "11208209120649888667",
"templateLink": null,
"timestamp": "2023-07-21T23:40:49.234299+00:00",
"validatedResources": null
},
"tags": {
"azd-env-name": "matell-2508"
},
"type": "Microsoft.Resources/deployments"
}
38 changes: 0 additions & 38 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,44 +1072,6 @@ func getDeploymentOptions(deployments []*azapi.ResourceDeployment) []string {
return promptValues
}

// resourceGroupsToDelete collects the resource groups from an existing deployment which should be removed as part of a
// destroy operation.
func resourceGroupsToDelete(deployment *azapi.ResourceDeployment) []string {
// NOTE: it's possible for a deployment to list a resource group more than once. We're only interested in the
// unique set.
resourceGroups := map[string]struct{}{}

if deployment.ProvisioningState == azapi.DeploymentProvisioningStateSucceeded {
// For a successful deployment, we can use the output resources property to see the resource groups that were
// provisioned from this.
for _, resourceId := range deployment.Resources {
if resourceId != nil && resourceId.ID != nil {
resId, err := arm.ParseResourceID(*resourceId.ID)
if err == nil && resId.ResourceGroupName != "" {
resourceGroups[resId.ResourceGroupName] = struct{}{}
}
}
}
} else {
// For a failed deployment, the `outputResources` field is not populated. Instead, we assume that any resource
// groups which this deployment itself deployed into should be deleted. This matches what a deployment likes
// for the common pattern of having a subscription level deployment which allocates a set of resource groups
// and then does nested deployments into them.
for _, dependency := range deployment.Dependencies {
if *dependency.ResourceType == string(azapi.AzureResourceTypeDeployment) {
for _, dependent := range dependency.DependsOn {
if *dependent.ResourceType == arm.ResourceGroupResourceType.String() {
resourceGroups[*dependent.ResourceName] = struct{}{}
}
}
}

}
}

return slices.Collect(maps.Keys(resourceGroups))
}

func (p *BicepProvider) generateResourcesToDelete(groupedResources map[string][]*azapi.Resource) []string {
lines := []string{"Resource(s) to be deleted:"}

Expand Down
Loading
Loading