diff --git a/eng/common/scripts/Helpers/Resource-Helpers.ps1 b/eng/common/scripts/Helpers/Resource-Helpers.ps1 index 5fca15845aeb..64ea0aeaac28 100644 --- a/eng/common/scripts/Helpers/Resource-Helpers.ps1 +++ b/eng/common/scripts/Helpers/Resource-Helpers.ps1 @@ -434,11 +434,17 @@ function RemoveStorageAccount($Account) { try { foreach ($container in $containers) { - $blobs = $container | Get-AzStorageBlob - foreach ($blob in $blobs) { - $shouldDelete = EnableBlobDeletion -Blob $blob -Container $container -StorageAccountName $Account.StorageAccountName -ResourceGroupName $Account.ResourceGroupName - if ($shouldDelete) { - $deleteNow += $blob + # VLW containers need version-aware cleanup: soft-delete causes deleted blobs to linger + # as non-current versions that block container deletion. See Remove-VlwContainerBlobs. + if (($container | Get-Member 'BlobContainerProperties') -and $container.BlobContainerProperties.HasImmutableStorageWithVersioning) { + Remove-VlwContainerBlobs -Container $container -StorageAccountName $Account.StorageAccountName -ResourceGroupName $Account.ResourceGroupName + } else { + $blobs = $container | Get-AzStorageBlob + foreach ($blob in $blobs) { + $shouldDelete = EnableBlobDeletion -Blob $blob -Container $container -StorageAccountName $Account.StorageAccountName -ResourceGroupName $Account.ResourceGroupName + if ($shouldDelete) { + $deleteNow += $blob + } } } } @@ -524,6 +530,41 @@ function EnableBlobDeletion($Blob, $Container, $StorageAccountName, $ResourceGro return $forceBlobDeletion } +# In VLW (Versioned-Level WORM) containers with soft-delete enabled, deleting a blob creates a +# non-current version instead of truly removing it. A standard Get-AzStorageBlob listing can't +# see these leftovers, but they still block container deletion (409 Conflict on the management +# plane DELETE). Listing with -IncludeVersion -IncludeDeleted makes them visible so we can clear +# immutability policies / legal holds and delete each version individually. Multiple passes handle +# new non-current versions that surface after each round of deletions. +function Remove-VlwContainerBlobs($Container, $StorageAccountName, $ResourceGroupName) { + Write-Host "Cleaning VLW container '$($Container.Name)' versions and soft-deleted blobs in account '$StorageAccountName', group: $ResourceGroupName" + + for ($round = 0; $round -lt 5; $round++) { + $found = $false + $blobs = @($Container | Get-AzStorageBlob -IncludeVersion -IncludeDeleted -ErrorAction SilentlyContinue) + + foreach ($blob in $blobs) { + $found = $true + + # Unconditionally clear legal holds and immutability policies. Errors are expected for + # soft-deleted blobs or blobs that don't have these set. + try { $blob | Set-AzStorageBlobLegalHold -DisableLegalHold | Out-Null } catch { } + try { $blob | Remove-AzStorageBlobImmutabilityPolicy | Out-Null } catch { } + try { + $blob | Remove-AzStorageBlob -Force + } catch { + # Deleting the current version by version ID returns 403 + # (OperationNotAllowedOnRootBlob); fall back to base blob deletion. + try { + Remove-AzStorageBlob -Container $Container.Name -Blob $blob.Name -Context $Container.Context -Force + } catch { } + } + } + + if (-not $found) { break } + } +} + function DoesSubnetOverlap([string]$ipOrCidr, [string]$overlapIp) { [System.Net.IPAddress]$overlapIpAddress = $overlapIp $parsed = $ipOrCidr -split '/' @@ -543,4 +584,4 @@ function DoesSubnetOverlap([string]$ipOrCidr, [string]$overlapIp) { } return $baseIp.Address -eq ($overlapIpAddress.Address -band ([System.Net.IPAddress]$mask).Address) -} +} \ No newline at end of file