Skip to content

Commit 8777b5d

Browse files
authored
fix resource discovery (#6356)
1 parent 6e91972 commit 8777b5d

File tree

5 files changed

+230
-135
lines changed

5 files changed

+230
-135
lines changed

cli/azd/pkg/azapi/standard_deployments.go

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"encoding/json"
99
"errors"
1010
"fmt"
11+
"maps"
1112
"net/url"
13+
"slices"
1214

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

346-
// Get the environment name from the deployment tags
347-
envName, has := subscriptionDeployment.Tags[azure.TagKeyAzdEnvName]
348-
if !has || envName == nil {
349-
return nil, fmt.Errorf("environment name not found in deployment tags")
350-
}
351-
352-
// Get all resource groups tagged with the azd-env-name tag
353-
resourceGroups, err := ds.resourceService.ListResourceGroup(ctx, subscriptionId, &ListResourceGroupOptions{
354-
TagFilter: &Filter{Key: azure.TagKeyAzdEnvName, Value: *envName},
355-
})
356-
357-
if err != nil {
358-
return nil, fmt.Errorf("listing resource groups: %w", err)
359-
}
360-
348+
resourceGroupNames := resourceGroupsFromDeployment(subscriptionDeployment)
361349
allResources := []*armresources.ResourceReference{}
362350

363-
// Find all the resources from all the resource groups
364-
for _, resourceGroup := range resourceGroups {
365-
366-
resources, err := ds.resourceService.ListResourceGroupResources(ctx, subscriptionId, resourceGroup.Name, nil)
351+
// Find all the resources from the deployment's resource groups
352+
for _, resourceGroupName := range resourceGroupNames {
353+
resources, err := ds.resourceService.ListResourceGroupResources(ctx, subscriptionId, resourceGroupName, nil)
367354
if err != nil {
368355
return nil, fmt.Errorf("listing resource group resources: %w", err)
369356
}
370357

371-
resourceGroupId := azure.ResourceGroupRID(subscriptionId, resourceGroup.Name)
358+
resourceGroupId := azure.ResourceGroupRID(subscriptionId, resourceGroupName)
372359
allResources = append(allResources, &armresources.ResourceReference{
373360
ID: &resourceGroupId,
374361
})
@@ -383,6 +370,39 @@ func (ds *StandardDeployments) ListSubscriptionDeploymentResources(
383370
return allResources, nil
384371
}
385372

373+
// resourceGroupsFromDeployment extracts the unique resource groups associated to a deployment.
374+
func resourceGroupsFromDeployment(deployment *ResourceDeployment) []string {
375+
resourceGroups := map[string]struct{}{}
376+
377+
if deployment.ProvisioningState == DeploymentProvisioningStateSucceeded {
378+
// For a successful deployment, use the output resources property to find the resource groups.
379+
for _, resourceId := range deployment.Resources {
380+
if resourceId != nil && resourceId.ID != nil {
381+
resId, err := arm.ParseResourceID(*resourceId.ID)
382+
if err == nil && resId.ResourceGroupName != "" {
383+
resourceGroups[resId.ResourceGroupName] = struct{}{}
384+
}
385+
}
386+
}
387+
} else {
388+
// For a failed deployment, the outputResources field is not populated.
389+
// Instead, look at the dependencies to find resource groups that this deployment deployed into.
390+
for _, dependency := range deployment.Dependencies {
391+
if dependency.ResourceType != nil && *dependency.ResourceType == string(AzureResourceTypeDeployment) {
392+
for _, dependent := range dependency.DependsOn {
393+
if dependent.ResourceType != nil && *dependent.ResourceType == arm.ResourceGroupResourceType.String() {
394+
if dependent.ResourceName != nil {
395+
resourceGroups[*dependent.ResourceName] = struct{}{}
396+
}
397+
}
398+
}
399+
}
400+
}
401+
}
402+
403+
return slices.Collect(maps.Keys(resourceGroups))
404+
}
405+
386406
func (ds *StandardDeployments) ListResourceGroupDeploymentResources(
387407
ctx context.Context,
388408
subscriptionId string,

cli/azd/pkg/azapi/standard_deployments_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ package azapi
55

66
import (
77
"context"
8+
"sort"
89
"testing"
910
"time"
1011

12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
13+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
1114
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
1215
"github.com/azure/azure-dev/cli/azd/test/mocks"
1316
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
1418
)
1519

1620
func Test_StandardDeployments_GenerateDeploymentName(t *testing.T) {
@@ -45,3 +49,79 @@ func Test_StandardDeployments_GenerateDeploymentName(t *testing.T) {
4549
assert.LessOrEqual(t, len(deploymentName), 64)
4650
}
4751
}
52+
53+
func TestResourceGroupsFromDeployment(t *testing.T) {
54+
t.Parallel()
55+
56+
t.Run("references used when no output resources", func(t *testing.T) {
57+
mockDeployment := &ResourceDeployment{
58+
//nolint:lll
59+
Id: "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
60+
//nolint:lll
61+
DeploymentId: "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
62+
Name: "matell-2508",
63+
Type: "Microsoft.Resources/deployments",
64+
Tags: map[string]*string{
65+
"azd-env-name": to.Ptr("matell-2508"),
66+
},
67+
ProvisioningState: DeploymentProvisioningStateFailed,
68+
Timestamp: time.Now(),
69+
Dependencies: []*armresources.Dependency{
70+
{
71+
//nolint:lll
72+
ID: to.Ptr(
73+
"/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg/providers/Microsoft.Resources/deployments/resources",
74+
),
75+
ResourceName: to.Ptr("resources"),
76+
ResourceType: to.Ptr("Microsoft.Resources/deployments"),
77+
DependsOn: []*armresources.BasicDependency{
78+
{
79+
//nolint:lll
80+
ID: to.Ptr(
81+
"/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg",
82+
),
83+
ResourceName: to.Ptr("matell-2508-rg"),
84+
ResourceType: to.Ptr("Microsoft.Resources/resourceGroups"),
85+
},
86+
},
87+
},
88+
},
89+
}
90+
91+
require.Equal(t, []string{"matell-2508-rg"}, resourceGroupsFromDeployment(mockDeployment))
92+
})
93+
94+
t.Run("duplicate resource groups ignored", func(t *testing.T) {
95+
96+
mockDeployment := ResourceDeployment{
97+
Id: "DEPLOYMENT_ID",
98+
Name: "test-env",
99+
Resources: []*armresources.ResourceReference{
100+
{
101+
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupA"),
102+
},
103+
{
104+
ID: to.Ptr(
105+
"/subscriptions/sub-id/resourceGroups/groupA/Microsoft.Storage/storageAccounts/storageAccount",
106+
),
107+
},
108+
{
109+
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupB"),
110+
},
111+
{
112+
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupB/Microsoft.web/sites/test"),
113+
},
114+
{
115+
ID: to.Ptr("/subscriptions/sub-id/resourceGroups/groupC"),
116+
},
117+
},
118+
ProvisioningState: DeploymentProvisioningStateSucceeded,
119+
Timestamp: time.Now(),
120+
}
121+
122+
groups := resourceGroupsFromDeployment(&mockDeployment)
123+
124+
sort.Strings(groups)
125+
require.Equal(t, []string{"groupA", "groupB", "groupC"}, groups)
126+
})
127+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
{
3+
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/providers/Microsoft.Resources/deployments/matell-2508-1689982746",
4+
"location": "eastus2",
5+
"name": "matell-2508-1689982746",
6+
"properties": {
7+
"correlationId": "84c13af8bdb9709a44dda61f93cce798",
8+
"debugSetting": null,
9+
"dependencies": [
10+
{
11+
"dependsOn": [
12+
{
13+
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg",
14+
"resourceName": "matell-2508-rg",
15+
"resourceType": "Microsoft.Resources/resourceGroups"
16+
}
17+
],
18+
"id": "/subscriptions/faa080af-c1d8-40ad-9cce-e1a450ca5b57/resourceGroups/matell-2508-rg/providers/Microsoft.Resources/deployments/resources",
19+
"resourceGroup": "matell-2508-rg",
20+
"resourceName": "resources",
21+
"resourceType": "Microsoft.Resources/deployments"
22+
}
23+
],
24+
"duration": "PT1M39.7689131S",
25+
"error": {
26+
"additionalInfo": null,
27+
"code": "DeploymentFailed",
28+
"details": [
29+
{
30+
"additionalInfo": null,
31+
"code": "DeploymentFailed",
32+
"details": null,
33+
"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.",
34+
"target": null
35+
}
36+
],
37+
"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.",
38+
"target": null
39+
},
40+
"mode": "Incremental",
41+
"onErrorDeployment": null,
42+
"outputResources": null,
43+
"outputs": null,
44+
"parameters": {
45+
"databasePassword": {
46+
"type": "SecureString"
47+
},
48+
"location": {
49+
"type": "String",
50+
"value": "eastus2"
51+
},
52+
"name": {
53+
"type": "String",
54+
"value": "matell-2508"
55+
},
56+
"secretKey": {
57+
"type": "SecureString"
58+
}
59+
},
60+
"parametersLink": null,
61+
"providers": [
62+
{
63+
"id": null,
64+
"namespace": "Microsoft.Resources",
65+
"providerAuthorizationConsentState": null,
66+
"registrationPolicy": null,
67+
"registrationState": null,
68+
"resourceTypes": [
69+
{
70+
"aliases": null,
71+
"apiProfiles": null,
72+
"apiVersions": null,
73+
"capabilities": null,
74+
"defaultApiVersion": null,
75+
"locationMappings": null,
76+
"locations": [
77+
"eastus2"
78+
],
79+
"properties": null,
80+
"resourceType": "resourceGroups",
81+
"zoneMappings": null
82+
},
83+
{
84+
"aliases": null,
85+
"apiProfiles": null,
86+
"apiVersions": null,
87+
"capabilities": null,
88+
"defaultApiVersion": null,
89+
"locationMappings": null,
90+
"locations": [
91+
null
92+
],
93+
"properties": null,
94+
"resourceType": "deployments",
95+
"zoneMappings": null
96+
}
97+
]
98+
}
99+
],
100+
"provisioningState": "Failed",
101+
"templateHash": "11208209120649888667",
102+
"templateLink": null,
103+
"timestamp": "2023-07-21T23:40:49.234299+00:00",
104+
"validatedResources": null
105+
},
106+
"tags": {
107+
"azd-env-name": "matell-2508"
108+
},
109+
"type": "Microsoft.Resources/deployments"
110+
}

cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,44 +1072,6 @@ func getDeploymentOptions(deployments []*azapi.ResourceDeployment) []string {
10721072
return promptValues
10731073
}
10741074

1075-
// resourceGroupsToDelete collects the resource groups from an existing deployment which should be removed as part of a
1076-
// destroy operation.
1077-
func resourceGroupsToDelete(deployment *azapi.ResourceDeployment) []string {
1078-
// NOTE: it's possible for a deployment to list a resource group more than once. We're only interested in the
1079-
// unique set.
1080-
resourceGroups := map[string]struct{}{}
1081-
1082-
if deployment.ProvisioningState == azapi.DeploymentProvisioningStateSucceeded {
1083-
// For a successful deployment, we can use the output resources property to see the resource groups that were
1084-
// provisioned from this.
1085-
for _, resourceId := range deployment.Resources {
1086-
if resourceId != nil && resourceId.ID != nil {
1087-
resId, err := arm.ParseResourceID(*resourceId.ID)
1088-
if err == nil && resId.ResourceGroupName != "" {
1089-
resourceGroups[resId.ResourceGroupName] = struct{}{}
1090-
}
1091-
}
1092-
}
1093-
} else {
1094-
// For a failed deployment, the `outputResources` field is not populated. Instead, we assume that any resource
1095-
// groups which this deployment itself deployed into should be deleted. This matches what a deployment likes
1096-
// for the common pattern of having a subscription level deployment which allocates a set of resource groups
1097-
// and then does nested deployments into them.
1098-
for _, dependency := range deployment.Dependencies {
1099-
if *dependency.ResourceType == string(azapi.AzureResourceTypeDeployment) {
1100-
for _, dependent := range dependency.DependsOn {
1101-
if *dependent.ResourceType == arm.ResourceGroupResourceType.String() {
1102-
resourceGroups[*dependent.ResourceName] = struct{}{}
1103-
}
1104-
}
1105-
}
1106-
1107-
}
1108-
}
1109-
1110-
return slices.Collect(maps.Keys(resourceGroups))
1111-
}
1112-
11131075
func (p *BicepProvider) generateResourcesToDelete(groupedResources map[string][]*azapi.Resource) []string {
11141076
lines := []string{"Resource(s) to be deleted:"}
11151077

0 commit comments

Comments
 (0)