Skip to content

Commit 626a938

Browse files
hemarinaCopilot
andauthored
add purge support for log analytics (#6566)
* purge log anlaytics * go mod tidy * Update cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go Co-authored-by: Copilot <[email protected]> * Update cli/azd/go.mod Co-authored-by: Copilot <[email protected]> * add tests * go mod tidy * add comment * add comment * address feedback * add comment * golangci-lint * rerun recording * re run recording for containerapp --------- Co-authored-by: Copilot <[email protected]>
1 parent 5ac9b07 commit 626a938

17 files changed

+12707
-7938
lines changed

cli/azd/.vscode/cspell-azd-dictionary.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ armcosmos
2929
armdeploymentstacks
3030
armmachinelearning
3131
armmsi
32+
armoperationalinsights
3233
armresourcegraph
3334
armsql
3435
aspnet
@@ -191,6 +192,7 @@ oneauth
191192
oneline
192193
onmicrosoft
193194
opentelemetry
195+
operationalinsights
194196
ostest
195197
osutil
196198
osversion

cli/azd/go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0
2020
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v3 v3.2.0
2121
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
22+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2
2223
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0
2324
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.1
2425
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
@@ -79,7 +80,7 @@ require (
7980
go.uber.org/atomic v1.11.0
8081
go.uber.org/multierr v1.11.0
8182
go.yaml.in/yaml/v3 v3.0.4
82-
golang.org/x/sys v0.38.0
83+
golang.org/x/sys v0.39.0
8384
google.golang.org/grpc v1.76.0
8485
google.golang.org/protobuf v1.36.10
8586
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
@@ -155,12 +156,12 @@ require (
155156
go.opentelemetry.io/otel/metric v1.38.0 // indirect
156157
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
157158
go.starlark.net v0.0.0-20250906160240-bf296ed553ea // indirect
158-
golang.org/x/crypto v0.45.0 // indirect
159+
golang.org/x/crypto v0.46.0 // indirect
159160
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
160-
golang.org/x/net v0.47.0 // indirect
161-
golang.org/x/sync v0.18.0 // indirect
162-
golang.org/x/term v0.37.0 // indirect
163-
golang.org/x/text v0.31.0 // indirect
161+
golang.org/x/net v0.48.0 // indirect
162+
golang.org/x/sync v0.19.0 // indirect
163+
golang.org/x/term v0.38.0 // indirect
164+
golang.org/x/text v0.32.0 // indirect
164165
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 // indirect
165166
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
166167
gopkg.in/yaml.v3 v3.0.1 // indirect

cli/azd/go.sum

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanage
6565
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
6666
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek=
6767
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw=
68+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2 h1:SFLbQmpdytToYZQJw5NqrZRwHPIGJmf5ZgjStbLfUuU=
69+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2 v2.0.2/go.mod h1:H3EFkhcVTisidszwtIkRDggjS2HmOIA26J3g8hDdHAY=
6870
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
6971
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
7072
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.1 h1:bcgO/crpp7wqI0Froi/I4C2fme7Vk/WLusbV399Do8I=
@@ -464,24 +466,24 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
464466
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
465467
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
466468
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
467-
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
468-
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
469+
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
470+
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
469471
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
470472
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
471473
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
472474
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
473475
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
474476
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
475477
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
476-
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
477-
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
478+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
479+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
478480
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
479481
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
480482
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
481483
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
482484
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
483-
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
484-
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
485+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
486+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
485487
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
486488
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
487489
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -495,18 +497,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
495497
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
496498
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
497499
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
498-
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
499-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
500+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
501+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
500502
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
501503
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
502-
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
503-
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
504+
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
505+
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
504506
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
505507
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
506508
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
507509
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
508-
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
509-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
510+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
511+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
510512
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
511513
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
512514
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

cli/azd/pkg/azapi/log_analytics.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package azapi
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
11+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights/v2"
12+
)
13+
14+
type AzCliLogAnalyticsWorkspace struct {
15+
Id string `json:"id"`
16+
Name string `json:"name"`
17+
}
18+
19+
func (cli *AzureClient) GetLogAnalyticsWorkspace(
20+
ctx context.Context,
21+
subscriptionId string,
22+
resourceGroupName string,
23+
workspaceName string,
24+
) (*AzCliLogAnalyticsWorkspace, error) {
25+
workspacesClient, err := cli.createLogAnalyticsWorkspacesClient(ctx, subscriptionId)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
workspace, err := workspacesClient.Get(ctx, resourceGroupName, workspaceName, nil)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed getting log analytics workspace: %w", err)
33+
}
34+
35+
return &AzCliLogAnalyticsWorkspace{
36+
Id: *workspace.ID,
37+
Name: *workspace.Name,
38+
}, nil
39+
}
40+
41+
func (cli *AzureClient) PurgeLogAnalyticsWorkspace(
42+
ctx context.Context,
43+
subscriptionId string,
44+
resourceGroupName string,
45+
workspaceName string,
46+
) error {
47+
workspacesClient, err := cli.createLogAnalyticsWorkspacesClient(ctx, subscriptionId)
48+
if err != nil {
49+
return err
50+
}
51+
52+
deleteOpts := &armoperationalinsights.WorkspacesClientBeginDeleteOptions{
53+
Force: to.Ptr(true),
54+
}
55+
56+
// https://learn.microsoft.com/rest/api/loganalytics/workspaces/delete?view=rest-loganalytics-2025-07-01&tabs=HTTP
57+
// Purging a workspace is done by setting the Force parameter to true when deleting the workspace
58+
poller, err := workspacesClient.BeginDelete(ctx, resourceGroupName, workspaceName, deleteOpts)
59+
if err != nil {
60+
return fmt.Errorf("starting purging log analytics workspace: %w", err)
61+
}
62+
63+
_, err = poller.PollUntilDone(ctx, nil)
64+
if err != nil {
65+
return fmt.Errorf("purging log analytics workspace: %w", err)
66+
}
67+
68+
return nil
69+
}
70+
71+
func (cli *AzureClient) createLogAnalyticsWorkspacesClient(
72+
ctx context.Context,
73+
subscriptionId string,
74+
) (*armoperationalinsights.WorkspacesClient, error) {
75+
credential, err := cli.credentialProvider.CredentialForSubscription(ctx, subscriptionId)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
client, err := armoperationalinsights.NewWorkspacesClient(subscriptionId, credential, cli.armClientOptions)
81+
if err != nil {
82+
return nil, fmt.Errorf("creating log analytics workspaces client: %w", err)
83+
}
84+
85+
return client, nil
86+
}

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,11 @@ func (p *BicepProvider) Destroy(
10051005
return nil, fmt.Errorf("getting cognitive accounts to purge: %w", err)
10061006
}
10071007

1008+
logAnalyticsWorkspaces, err := p.getLogAnalyticsWorkspacesToPurge(ctx, groupedResources)
1009+
if err != nil {
1010+
return nil, fmt.Errorf("getting log analytics workspaces to purge: %w", err)
1011+
}
1012+
10081013
p.console.StopSpinner(ctx, "", input.StepDone)
10091014

10101015
// Prompt for confirmation before deleting resources
@@ -1014,6 +1019,14 @@ func (p *BicepProvider) Destroy(
10141019

10151020
p.console.Message(ctx, output.WithGrayFormat("Deleting your resources can take some time.\n"))
10161021

1022+
// Force delete Log Analytics Workspaces first if purge is enabled
1023+
// This must happen before deleting resource groups since force delete requires the workspace to exist
1024+
if options.Purge() && len(logAnalyticsWorkspaces) > 0 {
1025+
if err := p.forceDeleteLogAnalyticsWorkspaces(ctx, logAnalyticsWorkspaces); err != nil {
1026+
return nil, fmt.Errorf("force deleting log analytics workspaces: %w", err)
1027+
}
1028+
}
1029+
10171030
if err := p.destroyDeployment(ctx, deploymentToDelete); err != nil {
10181031
return nil, fmt.Errorf("deleting resource groups: %w", err)
10191032
}
@@ -1571,6 +1584,33 @@ func (p *BicepProvider) getApiManagementsToPurge(
15711584
return apims, nil
15721585
}
15731586

1587+
func (p *BicepProvider) getLogAnalyticsWorkspacesToPurge(
1588+
ctx context.Context,
1589+
groupedResources map[string][]*azapi.Resource,
1590+
) ([]*azapi.AzCliLogAnalyticsWorkspace, error) {
1591+
workspaces := []*azapi.AzCliLogAnalyticsWorkspace{}
1592+
1593+
for resourceGroup, groupResources := range groupedResources {
1594+
for _, resource := range groupResources {
1595+
if resource.Type == string(azapi.AzureResourceTypeLogAnalyticsWorkspace) {
1596+
workspace, err := p.azapi.GetLogAnalyticsWorkspace(
1597+
ctx,
1598+
azure.SubscriptionFromRID(resource.Id),
1599+
resourceGroup,
1600+
resource.Name,
1601+
)
1602+
if err != nil {
1603+
return nil, fmt.Errorf("listing log analytics workspace %s properties: %w", resource.Name, err)
1604+
}
1605+
1606+
workspaces = append(workspaces, workspace)
1607+
}
1608+
}
1609+
}
1610+
1611+
return workspaces, nil
1612+
}
1613+
15741614
// Azure AppConfigurations have a "soft delete" functionality (now enabled by default) where a configuration store
15751615
// may be marked such that when it is deleted it can be recovered for a period of time. During that time,
15761616
// the name may not be reused.
@@ -1617,6 +1657,33 @@ func (p *BicepProvider) purgeAPIManagement(
16171657
return nil
16181658
}
16191659

1660+
// Handle Log Analytics Workspaces separately with Force option when purge is enabled.
1661+
// Unlike many other resources, Log Analytics Workspaces are not able to be purged after soft-delete
1662+
// because purge function only support for purge tables not a whole workspace and force delete must happen
1663+
// when their resource group is not deleted, so we must purge them explicitly before deleting the resource
1664+
func (p *BicepProvider) forceDeleteLogAnalyticsWorkspaces(
1665+
ctx context.Context,
1666+
workspaces []*azapi.AzCliLogAnalyticsWorkspace,
1667+
) error {
1668+
for _, workspace := range workspaces {
1669+
message := fmt.Sprintf("Purging Log Analytics Workspace: %s", output.WithHighLightFormat(workspace.Name))
1670+
p.console.ShowSpinner(ctx, message, input.Step)
1671+
1672+
err := p.azapi.PurgeLogAnalyticsWorkspace(
1673+
ctx,
1674+
azure.SubscriptionFromRID(workspace.Id),
1675+
*azure.GetResourceGroupName(workspace.Id),
1676+
workspace.Name,
1677+
)
1678+
1679+
p.console.StopSpinner(ctx, message, input.GetStepResultFormat(err))
1680+
if err != nil {
1681+
return fmt.Errorf("purging log analytics workspace %s: %w", workspace.Name, err)
1682+
}
1683+
}
1684+
return nil
1685+
}
1686+
16201687
type loadParametersResult struct {
16211688
parameters map[string]azure.ArmParameter
16221689
locationParams []string

0 commit comments

Comments
 (0)