From 51aab64fd1cff4202d9e690958cd6e0c75819920 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Tue, 14 Apr 2026 05:05:12 +0000 Subject: [PATCH 01/19] `azurerm_site_recovery_replicated_vm` - remove incorrect `ForceNew` from `managed_disk` disk type fields --- .../site_recovery_replicated_vm_resource.go | 3 - ...te_recovery_replicated_vm_resource_test.go | 92 +++++++++++++++++++ .../site_recovery_replicated_vm.html.markdown | 6 +- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index a465a4075816..eae90821b704 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -198,7 +198,6 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { Optional: true, Computed: true, ConfigMode: pluginsdk.SchemaConfigModeAttr, - ForceNew: true, Set: resourceSiteRecoveryReplicatedVMDiskHash, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ @@ -226,14 +225,12 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { "target_disk_type": { Type: pluginsdk.TypeString, Required: true, - ForceNew: true, ValidateFunc: validation.StringInSlice(replicationprotecteditems.PossibleValuesForDiskAccountType(), false), }, "target_replica_disk_type": { Type: pluginsdk.TypeString, Required: true, - ForceNew: true, ValidateFunc: validation.StringInSlice(replicationprotecteditems.PossibleValuesForDiskAccountType(), false), }, diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go index 246b81fbd709..772dd6cb1cb7 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go @@ -252,6 +252,32 @@ func TestAccSiteRecoveryReplicatedVm_targetVirtualMachineSize(t *testing.T) { }) } +func TestAccSiteRecoveryReplicatedVm_updateDiskType(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_site_recovery_replicated_vm", "test") + r := SiteRecoveryReplicatedVmResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.updateDiskType(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("managed_disk.0.target_disk_type").HasValue("Premium_LRS"), + check.That(data.ResourceName).Key("managed_disk.0.target_replica_disk_type").HasValue("Premium_LRS"), + ), + }, + data.ImportStep(), + { + Config: r.updateDiskTypeUpdated(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("managed_disk.0.target_disk_type").HasValue("StandardSSD_LRS"), + check.That(data.ResourceName).Key("managed_disk.0.target_replica_disk_type").HasValue("StandardSSD_LRS"), + ), + }, + data.ImportStep(), + }) +} + func (SiteRecoveryReplicatedVmResource) template(data acceptance.TestData) string { tags := "" if strings.HasPrefix(strings.ToLower(data.Client().SubscriptionID), "85b3dbca") { @@ -2515,6 +2541,72 @@ resource "azurerm_site_recovery_replicated_vm" "test" { `, r.vmSizeTemplate(data, "Standard_B2s"), data.RandomInteger) } +func (r SiteRecoveryReplicatedVmResource) updateDiskType(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_site_recovery_replicated_vm" "test" { + name = "repl-%[2]d" + resource_group_name = azurerm_resource_group.test2.name + recovery_vault_name = azurerm_recovery_services_vault.test.name + source_vm_id = azurerm_virtual_machine.test.id + source_recovery_fabric_name = azurerm_site_recovery_fabric.test1.name + recovery_replication_policy_id = azurerm_site_recovery_replication_policy.test.id + source_recovery_protection_container_name = azurerm_site_recovery_protection_container.test1.name + + target_resource_group_id = azurerm_resource_group.test2.id + target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id + target_recovery_protection_container_id = azurerm_site_recovery_protection_container.test2.id + + managed_disk { + disk_id = azurerm_virtual_machine.test.storage_os_disk[0].managed_disk_id + staging_storage_account_id = azurerm_storage_account.test.id + target_resource_group_id = azurerm_resource_group.test2.id + target_disk_type = "Premium_LRS" + target_replica_disk_type = "Premium_LRS" + } + + depends_on = [ + azurerm_site_recovery_protection_container_mapping.test, + azurerm_site_recovery_network_mapping.test, + ] +} +`, r.template(data), data.RandomInteger) +} + +func (r SiteRecoveryReplicatedVmResource) updateDiskTypeUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_site_recovery_replicated_vm" "test" { + name = "repl-%[2]d" + resource_group_name = azurerm_resource_group.test2.name + recovery_vault_name = azurerm_recovery_services_vault.test.name + source_vm_id = azurerm_virtual_machine.test.id + source_recovery_fabric_name = azurerm_site_recovery_fabric.test1.name + recovery_replication_policy_id = azurerm_site_recovery_replication_policy.test.id + source_recovery_protection_container_name = azurerm_site_recovery_protection_container.test1.name + + target_resource_group_id = azurerm_resource_group.test2.id + target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id + target_recovery_protection_container_id = azurerm_site_recovery_protection_container.test2.id + + managed_disk { + disk_id = azurerm_virtual_machine.test.storage_os_disk[0].managed_disk_id + staging_storage_account_id = azurerm_storage_account.test.id + target_resource_group_id = azurerm_resource_group.test2.id + target_disk_type = "StandardSSD_LRS" + target_replica_disk_type = "StandardSSD_LRS" + } + + depends_on = [ + azurerm_site_recovery_protection_container_mapping.test, + azurerm_site_recovery_network_mapping.test, + ] +} +`, r.template(data), data.RandomInteger) +} + func (r SiteRecoveryReplicatedVmResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := replicationprotecteditems.ParseReplicationProtectedItemID(state.ID) if err != nil { diff --git a/website/docs/r/site_recovery_replicated_vm.html.markdown b/website/docs/r/site_recovery_replicated_vm.html.markdown index eefbf8e5f209..665f425bb05b 100644 --- a/website/docs/r/site_recovery_replicated_vm.html.markdown +++ b/website/docs/r/site_recovery_replicated_vm.html.markdown @@ -246,7 +246,7 @@ The following arguments are supported: * `target_zone` - (Optional) Specifies the Availability Zone where the Failover VM should exist. Changing this forces a new resource to be created. -* `managed_disk` - (Optional) One or more `managed_disk` block as defined below. Changing this forces a new resource to be created. +* `managed_disk` - (Optional) One or more `managed_disk` block as defined below. * `unmanaged_disk` - (Optional) One or more `unmanaged_disk` block as defined below. Changing this forces a new resource to be created. @@ -280,9 +280,9 @@ A `managed_disk` block supports the following: * `target_resource_group_id` - (Required) Resource group disk should belong to when a failover is done. Changing this forces a new resource to be created. -* `target_disk_type` - (Required) What type should the disk be when a failover is done. Possible values are `Standard_LRS`, `Premium_LRS`, `PremiumV2_LRS`, `StandardSSD_LRS`, `UltraSSD_LRS`, `StandardSSD_ZRS` and `Premium_ZRS`. Changing this forces a new resource to be created. +* `target_disk_type` - (Required) What type should the disk be when a failover is done. Possible values are `Standard_LRS`, `Premium_LRS`, `PremiumV2_LRS`, `StandardSSD_LRS`, `UltraSSD_LRS`, `StandardSSD_ZRS` and `Premium_ZRS`. -* `target_replica_disk_type` - (Required) What type should the disk be that holds the replication data. Possible values are `Standard_LRS`, `Premium_LRS`, `PremiumV2_LRS`, `StandardSSD_LRS`, `UltraSSD_LRS`, `StandardSSD_ZRS` and `Premium_ZRS`. Changing this forces a new resource to be created. +* `target_replica_disk_type` - (Required) What type should the disk be that holds the replication data. Possible values are `Standard_LRS`, `Premium_LRS`, `PremiumV2_LRS`, `StandardSSD_LRS`, `UltraSSD_LRS`, `StandardSSD_ZRS` and `Premium_ZRS`. * `target_disk_encryption_set_id` - (Optional) The Disk Encryption Set that the Managed Disk will be associated with. Changing this forces a new resource to be created. From 2b52ed0b1fc3add32e16ab9b1826c3a58fe07fc7 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Wed, 6 May 2026 05:53:28 +0000 Subject: [PATCH 02/19] add poller for diskType change --- ...recovery_replicated_vm_disk_type_poller.go | 173 ++++++++++++++++++ .../site_recovery_replicated_vm_resource.go | 71 +++++++ ...te_recovery_replicated_vm_resource_test.go | 5 +- 3 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go new file mode 100644 index 000000000000..25b7d61c9f56 --- /dev/null +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -0,0 +1,173 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + +package custompollers + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2024-04-01/replicationprotecteditems" + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" +) + +var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} + +type SiteRecoveryReplicatedVMDiskTypeTarget struct { + DiskId string + TargetDiskType string + TargetReplicaDiskType string +} + +type SiteRecoveryReplicatedVMDiskTypePoller struct { + client *replicationprotecteditems.ReplicationProtectedItemsClient + id replicationprotecteditems.ReplicationProtectedItemId + targets map[string]SiteRecoveryReplicatedVMDiskTypeTarget + latestState string +} + +func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems.ReplicationProtectedItemsClient, id replicationprotecteditems.ReplicationProtectedItemId, targets []SiteRecoveryReplicatedVMDiskTypeTarget) *SiteRecoveryReplicatedVMDiskTypePoller { + targetsByDiskId := make(map[string]SiteRecoveryReplicatedVMDiskTypeTarget, len(targets)) + for _, target := range targets { + targetsByDiskId[normalizeSiteRecoveryReplicatedVMManagedDiskID(target.DiskId)] = target + } + + return &SiteRecoveryReplicatedVMDiskTypePoller{ + client: client, + id: id, + targets: targetsByDiskId, + } +} + +func (p *SiteRecoveryReplicatedVMDiskTypePoller) LatestState() string { + return p.latestState +} + +func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pollers.PollResult, error) { + resp, err := p.client.Get(ctx, p.id) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", p.id, err) + } + + if resp.Model == nil { + return nil, fmt.Errorf("retrieving %s: `model` was nil", p.id) + } + + if resp.Model.Properties == nil { + return nil, fmt.Errorf("retrieving %s: `properties` was nil", p.id) + } + + result := &pollers.PollResult{ + HttpResponse: &client.Response{ + OData: resp.OData, + Response: resp.HttpResponse, + }, + PollInterval: 30 * time.Second, + } + + if siteRecoveryReplicatedVMStateIndicatesFailure(pointer.From(resp.Model.Properties.ProtectionState)) { + return nil, pollers.PollingFailedError{ + HttpResponse: result.HttpResponse, + Message: fmt.Sprintf("%s entered protection state %q", p.id, pointer.From(resp.Model.Properties.ProtectionState)), + } + } + + a2aDetails, ok := resp.Model.Properties.ProviderSpecificDetails.(replicationprotecteditems.A2AReplicationDetails) + if !ok { + return nil, pollers.PollingFailedError{ + HttpResponse: result.HttpResponse, + Message: fmt.Sprintf("%s returned provider specific details that were not A2A", p.id), + } + } + + if siteRecoveryReplicatedVMStateIndicatesFailure(pointer.From(a2aDetails.VMProtectionState)) { + return nil, pollers.PollingFailedError{ + HttpResponse: result.HttpResponse, + Message: fmt.Sprintf("%s entered VM protection state %q", p.id, pointer.From(a2aDetails.VMProtectionState)), + } + } + + pending, err := p.pendingDiskTypeUpdates(a2aDetails, result.HttpResponse) + if err != nil { + return nil, err + } + if len(pending) == 0 { + p.latestState = "" + result.Status = pollers.PollingStatusSucceeded + return result, nil + } + + sort.Strings(pending) + p.latestState = strings.Join(pending, "; ") + result.Status = pollers.PollingStatusInProgress + return result, nil +} + +func (p *SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details replicationprotecteditems.A2AReplicationDetails, resp *client.Response) ([]string, error) { + pending := make([]string, 0) + + protectedDisks := make(map[string]replicationprotecteditems.A2AProtectedManagedDiskDetails) + if details.ProtectedManagedDisks != nil { + for _, disk := range *details.ProtectedManagedDisks { + diskId := normalizeSiteRecoveryReplicatedVMManagedDiskID(pointer.From(disk.DiskId)) + if _, ok := p.targets[diskId]; ok { + protectedDisks[diskId] = disk + } + } + } + + for diskId, target := range p.targets { + disk, ok := protectedDisks[diskId] + if !ok { + pending = append(pending, fmt.Sprintf("%s is not present in protected managed disks", target.DiskId)) + continue + } + + if siteRecoveryReplicatedVMStateIndicatesFailure(pointer.From(disk.DiskState)) { + p.latestState = fmt.Sprintf("%s entered disk state %q", target.DiskId, pointer.From(disk.DiskState)) + return nil, pollers.PollingFailedError{ + HttpResponse: resp, + Message: p.latestState, + } + } + + actualTargetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) + actualTargetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) + + mismatches := make([]string, 0) + if actualTargetDiskType != target.TargetDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_disk_type is %q, expected %q", actualTargetDiskType, target.TargetDiskType)) + } + if actualTargetReplicaDiskType != target.TargetReplicaDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_replica_disk_type is %q, expected %q", actualTargetReplicaDiskType, target.TargetReplicaDiskType)) + } + if len(mismatches) > 0 { + pending = append(pending, fmt.Sprintf("%s: %s", target.DiskId, strings.Join(mismatches, ", "))) + } + } + + return pending, nil +} + +func normalizeSiteRecoveryReplicatedVMManagedDiskID(input string) string { + if parsed, err := commonids.ParseManagedDiskIDInsensitively(input); err == nil { + return strings.ToLower(parsed.ID()) + } + + return strings.ToLower(input) +} + +func siteRecoveryReplicatedVMStateIndicatesFailure(input string) bool { + input = strings.ToLower(input) + if input == "" || input == "noerror" { + return false + } + + return strings.Contains(input, "failed") || strings.Contains(input, "error") +} diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index eae90821b704..1eca90b92ca2 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -26,11 +26,13 @@ import ( "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2024-04-01/replicationpolicies" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2024-04-01/replicationprotecteditems" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2024-04-01/replicationprotectioncontainers" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/features" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/custompollers" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/validate" resourceParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/parse" @@ -647,14 +649,83 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl }, } + diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(d) + err = client.UpdateThenPoll(ctx, id, parameters) if err != nil { return fmt.Errorf("updating replicated vm %s (vault %s): %+v", name, vaultName, err) } + if len(diskTypeTargets) > 0 { + diskTypePoller := custompollers.NewSiteRecoveryReplicatedVMDiskTypePoller(client, id, diskTypeTargets) + poller := pollers.NewPoller(diskTypePoller, 0, pollers.DefaultNumberOfDroppedConnectionsToAllow) + if err = poller.PollUntilDone(ctx); err != nil { + if latestState := diskTypePoller.LatestState(); latestState != "" { + return fmt.Errorf("waiting for managed disk type updates for replicated vm %s (vault %s): %s: %+v", name, vaultName, latestState, err) + } + + return fmt.Errorf("waiting for managed disk type updates for replicated vm %s (vault %s): %+v", name, vaultName, err) + } + } + return resourceSiteRecoveryReplicatedItemRead(d, meta) } +func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(d *pluginsdk.ResourceData) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { + if !d.HasChange("managed_disk") { + return nil + } + + oldRaw, newRaw := d.GetChange("managed_disk") + oldDisks, ok := oldRaw.(*pluginsdk.Set) + if !ok || oldDisks == nil { + return nil + } + + newDisks, ok := newRaw.(*pluginsdk.Set) + if !ok || newDisks == nil { + return nil + } + + oldByDiskId := make(map[string]map[string]interface{}, oldDisks.Len()) + for _, raw := range oldDisks.List() { + disk := raw.(map[string]interface{}) + oldByDiskId[siteRecoveryReplicatedVMManagedDiskID(disk)] = disk + } + + targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeTarget, 0) + for _, raw := range newDisks.List() { + disk := raw.(map[string]interface{}) + oldDisk, ok := oldByDiskId[siteRecoveryReplicatedVMManagedDiskID(disk)] + if !ok { + continue + } + + targetDiskType := disk["target_disk_type"].(string) + targetReplicaDiskType := disk["target_replica_disk_type"].(string) + if targetDiskType == oldDisk["target_disk_type"].(string) && targetReplicaDiskType == oldDisk["target_replica_disk_type"].(string) { + continue + } + + targets = append(targets, custompollers.SiteRecoveryReplicatedVMDiskTypeTarget{ + DiskId: disk["disk_id"].(string), + TargetDiskType: targetDiskType, + TargetReplicaDiskType: targetReplicaDiskType, + }) + } + + return targets +} + +func siteRecoveryReplicatedVMManagedDiskID(disk map[string]interface{}) string { + diskId := disk["disk_id"].(string) + if parsed, err := commonids.ParseManagedDiskIDInsensitively(diskId); err == nil { + return strings.ToLower(parsed.ID()) + } + + return strings.ToLower(diskId) +} + func findNicId(state *replicationprotecteditems.ReplicationProtectedItem, sourceNicId string) *string { if a2aDetails, isA2a := state.Properties.ProviderSpecificDetails.(replicationprotecteditems.A2AReplicationDetails); isA2a { if a2aDetails.VMNics != nil { diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go index 4d803b6e6258..9a9bd2423c42 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource_test.go @@ -1676,6 +1676,7 @@ resource "azurerm_recovery_services_vault" "test" { name = "acctest-vault-%[1]d" location = azurerm_resource_group.test2.location resource_group_name = azurerm_resource_group.test2.name + sku = "Standard" } resource "azurerm_site_recovery_fabric" "test1" { resource_group_name = azurerm_resource_group.test2.name @@ -2546,7 +2547,7 @@ func (r SiteRecoveryReplicatedVmResource) updateDiskType(data acceptance.TestDat %s resource "azurerm_site_recovery_replicated_vm" "test" { - name = "repl-%[2]d" + name = "acctest-repl-%[2]d" resource_group_name = azurerm_resource_group.test2.name recovery_vault_name = azurerm_recovery_services_vault.test.name source_vm_id = azurerm_virtual_machine.test.id @@ -2579,7 +2580,7 @@ func (r SiteRecoveryReplicatedVmResource) updateDiskTypeUpdated(data acceptance. %s resource "azurerm_site_recovery_replicated_vm" "test" { - name = "repl-%[2]d" + name = "acctest-repl-%[2]d" resource_group_name = azurerm_resource_group.test2.name recovery_vault_name = azurerm_recovery_services_vault.test.name source_vm_id = azurerm_virtual_machine.test.id From 60967e5ddfd5d61f1e52690d0674f467fba6fb78 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Mon, 11 May 2026 02:46:02 +0000 Subject: [PATCH 03/19] update for disk type update --- .../site_recovery_replicated_vm_resource.go | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 1eca90b92ca2..b4069892a135 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -649,7 +649,7 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl }, } - diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(d) + diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks, state) err = client.UpdateThenPoll(ctx, id, parameters) if err != nil { @@ -671,44 +671,42 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl return resourceSiteRecoveryReplicatedItemRead(d, meta) } -func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(d *pluginsdk.ResourceData) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { - if !d.HasChange("managed_disk") { +func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks []replicationprotecteditems.A2AVMManagedDiskUpdateDetails, state *replicationprotecteditems.ReplicationProtectedItem) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { + if len(managedDisks) == 0 || state == nil || state.Properties == nil { return nil } - oldRaw, newRaw := d.GetChange("managed_disk") - oldDisks, ok := oldRaw.(*pluginsdk.Set) - if !ok || oldDisks == nil { - return nil - } - - newDisks, ok := newRaw.(*pluginsdk.Set) - if !ok || newDisks == nil { + a2aDetails, ok := state.Properties.ProviderSpecificDetails.(replicationprotecteditems.A2AReplicationDetails) + if !ok { return nil } - oldByDiskId := make(map[string]map[string]interface{}, oldDisks.Len()) - for _, raw := range oldDisks.List() { - disk := raw.(map[string]interface{}) - oldByDiskId[siteRecoveryReplicatedVMManagedDiskID(disk)] = disk + currentDisks := make(map[string]replicationprotecteditems.A2AProtectedManagedDiskDetails) + if a2aDetails.ProtectedManagedDisks != nil { + for _, disk := range *a2aDetails.ProtectedManagedDisks { + if diskId := pointer.From(disk.DiskId); diskId != "" { + currentDisks[siteRecoveryReplicatedVMManagedDiskID(diskId)] = disk + } + } } targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeTarget, 0) - for _, raw := range newDisks.List() { - disk := raw.(map[string]interface{}) - oldDisk, ok := oldByDiskId[siteRecoveryReplicatedVMManagedDiskID(disk)] - if !ok { + for _, disk := range managedDisks { + diskId := pointer.From(disk.DiskId) + if diskId == "" { continue } - targetDiskType := disk["target_disk_type"].(string) - targetReplicaDiskType := disk["target_replica_disk_type"].(string) - if targetDiskType == oldDisk["target_disk_type"].(string) && targetReplicaDiskType == oldDisk["target_replica_disk_type"].(string) { + targetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) + targetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) + if currentDisk, ok := currentDisks[siteRecoveryReplicatedVMManagedDiskID(diskId)]; ok && + pointer.From(currentDisk.RecoveryTargetDiskAccountType) == targetDiskType && + pointer.From(currentDisk.RecoveryReplicaDiskAccountType) == targetReplicaDiskType { continue } targets = append(targets, custompollers.SiteRecoveryReplicatedVMDiskTypeTarget{ - DiskId: disk["disk_id"].(string), + DiskId: diskId, TargetDiskType: targetDiskType, TargetReplicaDiskType: targetReplicaDiskType, }) @@ -717,8 +715,7 @@ func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(d *pluginsdk.ResourceD return targets } -func siteRecoveryReplicatedVMManagedDiskID(disk map[string]interface{}) string { - diskId := disk["disk_id"].(string) +func siteRecoveryReplicatedVMManagedDiskID(diskId string) string { if parsed, err := commonids.ParseManagedDiskIDInsensitively(diskId); err == nil { return strings.ToLower(parsed.ID()) } From 5f4cd6efa5a63cf196bd2ca0c5422030c5b1e031 Mon Sep 17 00:00:00 2001 From: Gerry Tan <2316198+gerrytan@users.noreply.github.com> Date: Mon, 11 May 2026 13:24:19 +1000 Subject: [PATCH 04/19] Update goenv before installing a version --- .teamcity/components/build_components.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.teamcity/components/build_components.kt b/.teamcity/components/build_components.kt index e2e7403664d1..98e27c3bf48f 100644 --- a/.teamcity/components/build_components.kt +++ b/.teamcity/components/build_components.kt @@ -33,7 +33,7 @@ fun BuildFeatures.BuildCacheFeature() { fun BuildSteps.ConfigureGoEnv() { step(ScriptBuildStep { name = "Configure Go Version" - scriptContent = "goenv install -s \$(goenv local) && goenv rehash" + scriptContent = "cd $(goenv root) && git pull && cd - && goenv install -s \$(goenv local) && goenv rehash" }) } From 5ec55bdf4ddf42cf99f6e6bc344aecaf4d3daccf Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 14 May 2026 03:49:26 +0000 Subject: [PATCH 05/19] Revert "Update goenv before installing a version" This reverts commit 5f4cd6efa5a63cf196bd2ca0c5422030c5b1e031. --- .teamcity/components/build_components.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.teamcity/components/build_components.kt b/.teamcity/components/build_components.kt index 98e27c3bf48f..e2e7403664d1 100644 --- a/.teamcity/components/build_components.kt +++ b/.teamcity/components/build_components.kt @@ -33,7 +33,7 @@ fun BuildFeatures.BuildCacheFeature() { fun BuildSteps.ConfigureGoEnv() { step(ScriptBuildStep { name = "Configure Go Version" - scriptContent = "cd $(goenv root) && git pull && cd - && goenv install -s \$(goenv local) && goenv rehash" + scriptContent = "goenv install -s \$(goenv local) && goenv rehash" }) } From 53a61bbddfa132d884b222707eaef1bc09377a1a Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 14 May 2026 04:28:56 +0000 Subject: [PATCH 06/19] update poller --- .../site_recovery_replicated_vm_resource.go | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index b4069892a135..6eaaf1cbaff4 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -649,7 +649,7 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl }, } - diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks, state) + diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks) err = client.UpdateThenPoll(ctx, id, parameters) if err != nil { @@ -671,56 +671,30 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl return resourceSiteRecoveryReplicatedItemRead(d, meta) } -func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks []replicationprotecteditems.A2AVMManagedDiskUpdateDetails, state *replicationprotecteditems.ReplicationProtectedItem) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { - if len(managedDisks) == 0 || state == nil || state.Properties == nil { +func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks []replicationprotecteditems.A2AVMManagedDiskUpdateDetails) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { + if len(managedDisks) == 0 { return nil } - a2aDetails, ok := state.Properties.ProviderSpecificDetails.(replicationprotecteditems.A2AReplicationDetails) - if !ok { - return nil - } - - currentDisks := make(map[string]replicationprotecteditems.A2AProtectedManagedDiskDetails) - if a2aDetails.ProtectedManagedDisks != nil { - for _, disk := range *a2aDetails.ProtectedManagedDisks { - if diskId := pointer.From(disk.DiskId); diskId != "" { - currentDisks[siteRecoveryReplicatedVMManagedDiskID(diskId)] = disk - } - } - } - - targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeTarget, 0) + targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeTarget, 0, len(managedDisks)) for _, disk := range managedDisks { diskId := pointer.From(disk.DiskId) if diskId == "" { continue } - targetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) - targetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) - if currentDisk, ok := currentDisks[siteRecoveryReplicatedVMManagedDiskID(diskId)]; ok && - pointer.From(currentDisk.RecoveryTargetDiskAccountType) == targetDiskType && - pointer.From(currentDisk.RecoveryReplicaDiskAccountType) == targetReplicaDiskType { - continue - } - targets = append(targets, custompollers.SiteRecoveryReplicatedVMDiskTypeTarget{ DiskId: diskId, - TargetDiskType: targetDiskType, - TargetReplicaDiskType: targetReplicaDiskType, + TargetDiskType: pointer.From(disk.RecoveryTargetDiskAccountType), + TargetReplicaDiskType: pointer.From(disk.RecoveryReplicaDiskAccountType), }) } - return targets -} - -func siteRecoveryReplicatedVMManagedDiskID(diskId string) string { - if parsed, err := commonids.ParseManagedDiskIDInsensitively(diskId); err == nil { - return strings.ToLower(parsed.ID()) + if len(targets) == 0 { + return nil } - return strings.ToLower(diskId) + return targets } func findNicId(state *replicationprotecteditems.ReplicationProtectedItem, sourceNicId string) *string { From c931ed8a0c8a22333fbf239da08bbbd0bf303f83 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 28 May 2026 04:05:14 +0000 Subject: [PATCH 07/19] update poller --- ...recovery_replicated_vm_disk_type_poller.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go index 25b7d61c9f56..eb22ffc8ba2b 100644 --- a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -19,6 +19,8 @@ import ( var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} +const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 3 + type SiteRecoveryReplicatedVMDiskTypeTarget struct { DiskId string TargetDiskType string @@ -26,10 +28,11 @@ type SiteRecoveryReplicatedVMDiskTypeTarget struct { } type SiteRecoveryReplicatedVMDiskTypePoller struct { - client *replicationprotecteditems.ReplicationProtectedItemsClient - id replicationprotecteditems.ReplicationProtectedItemId - targets map[string]SiteRecoveryReplicatedVMDiskTypeTarget - latestState string + client *replicationprotecteditems.ReplicationProtectedItemsClient + id replicationprotecteditems.ReplicationProtectedItemId + targets map[string]SiteRecoveryReplicatedVMDiskTypeTarget + latestState string + consecutiveSuccessfulReads int } func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems.ReplicationProtectedItemsClient, id replicationprotecteditems.ReplicationProtectedItemId, targets []SiteRecoveryReplicatedVMDiskTypeTarget) *SiteRecoveryReplicatedVMDiskTypePoller { @@ -98,11 +101,19 @@ func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pol return nil, err } if len(pending) == 0 { + p.consecutiveSuccessfulReads++ + if p.consecutiveSuccessfulReads < siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads { + p.latestState = fmt.Sprintf("managed disk types matched target values for %d consecutive reads", p.consecutiveSuccessfulReads) + result.Status = pollers.PollingStatusInProgress + return result, nil + } + p.latestState = "" result.Status = pollers.PollingStatusSucceeded return result, nil } + p.consecutiveSuccessfulReads = 0 sort.Strings(pending) p.latestState = strings.Join(pending, "; ") result.Status = pollers.PollingStatusInProgress From ccf807b1d7357aab598b800f167eac057944f751 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 28 May 2026 04:26:31 +0000 Subject: [PATCH 08/19] fmt --- .../recoveryservices/site_recovery_replicated_vm_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 623bab97d62d..ab7231499e05 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -32,8 +32,8 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/features" - "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/custompollers" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/custompollers" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/validate" resourceParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/parse" From 33ddf4ae97bff2ef140e526e786e68b888055d2b Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 29 May 2026 03:25:07 +0000 Subject: [PATCH 09/19] update custom poller to monitor disk migration job --- ...recovery_replicated_vm_disk_type_poller.go | 94 +++++++++---------- .../site_recovery_replicated_vm_resource.go | 16 ++-- 2 files changed, 51 insertions(+), 59 deletions(-) diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go index eb22ffc8ba2b..cb759674b712 100644 --- a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -6,6 +6,7 @@ package custompollers import ( "context" "fmt" + "log" "sort" "strings" "time" @@ -19,40 +20,32 @@ import ( var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} -const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 3 - -type SiteRecoveryReplicatedVMDiskTypeTarget struct { - DiskId string - TargetDiskType string - TargetReplicaDiskType string +type SiteRecoveryReplicatedVMDiskTypeUpdate struct { + DiskId string + RecoveryTargetDiskType string + RecoveryReplicaDiskType string } type SiteRecoveryReplicatedVMDiskTypePoller struct { - client *replicationprotecteditems.ReplicationProtectedItemsClient - id replicationprotecteditems.ReplicationProtectedItemId - targets map[string]SiteRecoveryReplicatedVMDiskTypeTarget - latestState string - consecutiveSuccessfulReads int + client *replicationprotecteditems.ReplicationProtectedItemsClient + id replicationprotecteditems.ReplicationProtectedItemId + updates map[string]SiteRecoveryReplicatedVMDiskTypeUpdate } -func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems.ReplicationProtectedItemsClient, id replicationprotecteditems.ReplicationProtectedItemId, targets []SiteRecoveryReplicatedVMDiskTypeTarget) *SiteRecoveryReplicatedVMDiskTypePoller { - targetsByDiskId := make(map[string]SiteRecoveryReplicatedVMDiskTypeTarget, len(targets)) - for _, target := range targets { - targetsByDiskId[normalizeSiteRecoveryReplicatedVMManagedDiskID(target.DiskId)] = target +func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems.ReplicationProtectedItemsClient, id replicationprotecteditems.ReplicationProtectedItemId, updates []SiteRecoveryReplicatedVMDiskTypeUpdate) *SiteRecoveryReplicatedVMDiskTypePoller { + updatesByDiskId := make(map[string]SiteRecoveryReplicatedVMDiskTypeUpdate, len(updates)) + for _, update := range updates { + updatesByDiskId[normalizeSiteRecoveryReplicatedVMManagedDiskID(update.DiskId)] = update } return &SiteRecoveryReplicatedVMDiskTypePoller{ client: client, id: id, - targets: targetsByDiskId, + updates: updatesByDiskId, } } -func (p *SiteRecoveryReplicatedVMDiskTypePoller) LatestState() string { - return p.latestState -} - -func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pollers.PollResult, error) { +func (p SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pollers.PollResult, error) { resp, err := p.client.Get(ctx, p.id) if err != nil { return nil, fmt.Errorf("retrieving %s: %+v", p.id, err) @@ -100,72 +93,75 @@ func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pol if err != nil { return nil, err } - if len(pending) == 0 { - p.consecutiveSuccessfulReads++ - if p.consecutiveSuccessfulReads < siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads { - p.latestState = fmt.Sprintf("managed disk types matched target values for %d consecutive reads", p.consecutiveSuccessfulReads) - result.Status = pollers.PollingStatusInProgress - return result, nil - } - p.latestState = "" + if len(pending) == 0 { result.Status = pollers.PollingStatusSucceeded return result, nil } - p.consecutiveSuccessfulReads = 0 sort.Strings(pending) - p.latestState = strings.Join(pending, "; ") + log.Printf("[DEBUG] waiting for managed disk type updates for %s: %s", p.id, strings.Join(pending, "; ")) result.Status = pollers.PollingStatusInProgress return result, nil } -func (p *SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details replicationprotecteditems.A2AReplicationDetails, resp *client.Response) ([]string, error) { +func (p SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details replicationprotecteditems.A2AReplicationDetails, resp *client.Response) ([]string, error) { pending := make([]string, 0) protectedDisks := make(map[string]replicationprotecteditems.A2AProtectedManagedDiskDetails) if details.ProtectedManagedDisks != nil { for _, disk := range *details.ProtectedManagedDisks { diskId := normalizeSiteRecoveryReplicatedVMManagedDiskID(pointer.From(disk.DiskId)) - if _, ok := p.targets[diskId]; ok { + if _, ok := p.updates[diskId]; ok { protectedDisks[diskId] = disk } } } - for diskId, target := range p.targets { + for diskId, update := range p.updates { disk, ok := protectedDisks[diskId] if !ok { - pending = append(pending, fmt.Sprintf("%s is not present in protected managed disks", target.DiskId)) + pending = append(pending, fmt.Sprintf("%s is not present in protected managed disks", update.DiskId)) continue } if siteRecoveryReplicatedVMStateIndicatesFailure(pointer.From(disk.DiskState)) { - p.latestState = fmt.Sprintf("%s entered disk state %q", target.DiskId, pointer.From(disk.DiskState)) return nil, pollers.PollingFailedError{ HttpResponse: resp, - Message: p.latestState, + Message: fmt.Sprintf("%s entered disk state %q", update.DiskId, pointer.From(disk.DiskState)), } } - actualTargetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) - actualTargetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) - - mismatches := make([]string, 0) - if actualTargetDiskType != target.TargetDiskType { - mismatches = append(mismatches, fmt.Sprintf("target_disk_type is %q, expected %q", actualTargetDiskType, target.TargetDiskType)) - } - if actualTargetReplicaDiskType != target.TargetReplicaDiskType { - mismatches = append(mismatches, fmt.Sprintf("target_replica_disk_type is %q, expected %q", actualTargetReplicaDiskType, target.TargetReplicaDiskType)) - } - if len(mismatches) > 0 { - pending = append(pending, fmt.Sprintf("%s: %s", target.DiskId, strings.Join(mismatches, ", "))) + if pendingReason := siteRecoveryReplicatedVMDiskTypeUpdatePendingReason(update, disk); pendingReason != "" { + pending = append(pending, pendingReason) } } return pending, nil } +func siteRecoveryReplicatedVMDiskTypeUpdatePendingReason(update SiteRecoveryReplicatedVMDiskTypeUpdate, disk replicationprotecteditems.A2AProtectedManagedDiskDetails) string { + if jobType, percentageComplete := pointer.From(disk.MonitoringJobType), pointer.From(disk.MonitoringPercentageCompletion); jobType != "" && percentageComplete < 100 { + return fmt.Sprintf("%s: disk job %q is in progress (%d%% complete)", update.DiskId, jobType, percentageComplete) + } + + actualTargetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) + actualTargetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) + + mismatches := make([]string, 0) + if actualTargetDiskType != update.RecoveryTargetDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_disk_type is %q, expected %q", actualTargetDiskType, update.RecoveryTargetDiskType)) + } + if actualTargetReplicaDiskType != update.RecoveryReplicaDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_replica_disk_type is %q, expected %q", actualTargetReplicaDiskType, update.RecoveryReplicaDiskType)) + } + if len(mismatches) > 0 { + return fmt.Sprintf("%s: %s", update.DiskId, strings.Join(mismatches, ", ")) + } + + return "" +} + func normalizeSiteRecoveryReplicatedVMManagedDiskID(input string) string { if parsed, err := commonids.ParseManagedDiskIDInsensitively(input); err == nil { return strings.ToLower(parsed.ID()) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index ab7231499e05..f900c0629bb3 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -659,10 +659,6 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl diskTypePoller := custompollers.NewSiteRecoveryReplicatedVMDiskTypePoller(client, id, diskTypeTargets) poller := pollers.NewPoller(diskTypePoller, 0, pollers.DefaultNumberOfDroppedConnectionsToAllow) if err = poller.PollUntilDone(ctx); err != nil { - if latestState := diskTypePoller.LatestState(); latestState != "" { - return fmt.Errorf("waiting for managed disk type updates for replicated vm %s (vault %s): %s: %+v", name, vaultName, latestState, err) - } - return fmt.Errorf("waiting for managed disk type updates for replicated vm %s (vault %s): %+v", name, vaultName, err) } } @@ -670,22 +666,22 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl return resourceSiteRecoveryReplicatedItemRead(d, meta) } -func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks []replicationprotecteditems.A2AVMManagedDiskUpdateDetails) []custompollers.SiteRecoveryReplicatedVMDiskTypeTarget { +func siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks []replicationprotecteditems.A2AVMManagedDiskUpdateDetails) []custompollers.SiteRecoveryReplicatedVMDiskTypeUpdate { if len(managedDisks) == 0 { return nil } - targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeTarget, 0, len(managedDisks)) + targets := make([]custompollers.SiteRecoveryReplicatedVMDiskTypeUpdate, 0, len(managedDisks)) for _, disk := range managedDisks { diskId := pointer.From(disk.DiskId) if diskId == "" { continue } - targets = append(targets, custompollers.SiteRecoveryReplicatedVMDiskTypeTarget{ - DiskId: diskId, - TargetDiskType: pointer.From(disk.RecoveryTargetDiskAccountType), - TargetReplicaDiskType: pointer.From(disk.RecoveryReplicaDiskAccountType), + targets = append(targets, custompollers.SiteRecoveryReplicatedVMDiskTypeUpdate{ + DiskId: diskId, + RecoveryTargetDiskType: pointer.From(disk.RecoveryTargetDiskAccountType), + RecoveryReplicaDiskType: pointer.From(disk.RecoveryReplicaDiskAccountType), }) } From 99e72f5131b36c197b9a413600f604fc77dba531 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:29:31 +0000 Subject: [PATCH 10/19] update poller --- ...recovery_replicated_vm_disk_type_poller.go | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go index cb759674b712..5b4367eb2b2f 100644 --- a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -20,6 +20,8 @@ import ( var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} +const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 3 + type SiteRecoveryReplicatedVMDiskTypeUpdate struct { DiskId string RecoveryTargetDiskType string @@ -27,9 +29,10 @@ type SiteRecoveryReplicatedVMDiskTypeUpdate struct { } type SiteRecoveryReplicatedVMDiskTypePoller struct { - client *replicationprotecteditems.ReplicationProtectedItemsClient - id replicationprotecteditems.ReplicationProtectedItemId - updates map[string]SiteRecoveryReplicatedVMDiskTypeUpdate + client *replicationprotecteditems.ReplicationProtectedItemsClient + id replicationprotecteditems.ReplicationProtectedItemId + updates map[string]SiteRecoveryReplicatedVMDiskTypeUpdate + consecutiveSuccessfulReads int } func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems.ReplicationProtectedItemsClient, id replicationprotecteditems.ReplicationProtectedItemId, updates []SiteRecoveryReplicatedVMDiskTypeUpdate) *SiteRecoveryReplicatedVMDiskTypePoller { @@ -45,7 +48,7 @@ func NewSiteRecoveryReplicatedVMDiskTypePoller(client *replicationprotecteditems } } -func (p SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pollers.PollResult, error) { +func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pollers.PollResult, error) { resp, err := p.client.Get(ctx, p.id) if err != nil { return nil, fmt.Errorf("retrieving %s: %+v", p.id, err) @@ -95,17 +98,25 @@ func (p SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*poll } if len(pending) == 0 { + p.consecutiveSuccessfulReads++ + if p.consecutiveSuccessfulReads < siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads { + log.Printf("[DEBUG] waiting for managed disk type updates for %s: managed disk types matched target values for %d consecutive reads", p.id, p.consecutiveSuccessfulReads) + result.Status = pollers.PollingStatusInProgress + return result, nil + } + result.Status = pollers.PollingStatusSucceeded return result, nil } + p.consecutiveSuccessfulReads = 0 sort.Strings(pending) log.Printf("[DEBUG] waiting for managed disk type updates for %s: %s", p.id, strings.Join(pending, "; ")) result.Status = pollers.PollingStatusInProgress return result, nil } -func (p SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details replicationprotecteditems.A2AReplicationDetails, resp *client.Response) ([]string, error) { +func (p *SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details replicationprotecteditems.A2AReplicationDetails, resp *client.Response) ([]string, error) { pending := make([]string, 0) protectedDisks := make(map[string]replicationprotecteditems.A2AProtectedManagedDiskDetails) From a99176beea93aba6c648fb153cecf81ad493171e Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:19:12 +0000 Subject: [PATCH 11/19] update poller --- ...recovery_replicated_vm_disk_type_poller.go | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go index 5b4367eb2b2f..f7230cc7cd3b 100644 --- a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -20,7 +20,7 @@ import ( var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} -const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 3 +const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 10 type SiteRecoveryReplicatedVMDiskTypeUpdate struct { DiskId string @@ -100,7 +100,7 @@ func (p *SiteRecoveryReplicatedVMDiskTypePoller) Poll(ctx context.Context) (*pol if len(pending) == 0 { p.consecutiveSuccessfulReads++ if p.consecutiveSuccessfulReads < siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads { - log.Printf("[DEBUG] waiting for managed disk type updates for %s: managed disk types matched target values for %d consecutive reads", p.id, p.consecutiveSuccessfulReads) + log.Printf("[DEBUG] waiting for managed disk type updates for %s: matched %d/%d consecutive reads", p.id, p.consecutiveSuccessfulReads, siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads) result.Status = pollers.PollingStatusInProgress return result, nil } @@ -143,36 +143,24 @@ func (p *SiteRecoveryReplicatedVMDiskTypePoller) pendingDiskTypeUpdates(details } } - if pendingReason := siteRecoveryReplicatedVMDiskTypeUpdatePendingReason(update, disk); pendingReason != "" { - pending = append(pending, pendingReason) + actualTargetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) + actualTargetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) + + mismatches := make([]string, 0) + if actualTargetDiskType != update.RecoveryTargetDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_disk_type is %q, expected %q", actualTargetDiskType, update.RecoveryTargetDiskType)) + } + if actualTargetReplicaDiskType != update.RecoveryReplicaDiskType { + mismatches = append(mismatches, fmt.Sprintf("target_replica_disk_type is %q, expected %q", actualTargetReplicaDiskType, update.RecoveryReplicaDiskType)) + } + if len(mismatches) > 0 { + pending = append(pending, fmt.Sprintf("%s: %s", update.DiskId, strings.Join(mismatches, ", "))) } } return pending, nil } -func siteRecoveryReplicatedVMDiskTypeUpdatePendingReason(update SiteRecoveryReplicatedVMDiskTypeUpdate, disk replicationprotecteditems.A2AProtectedManagedDiskDetails) string { - if jobType, percentageComplete := pointer.From(disk.MonitoringJobType), pointer.From(disk.MonitoringPercentageCompletion); jobType != "" && percentageComplete < 100 { - return fmt.Sprintf("%s: disk job %q is in progress (%d%% complete)", update.DiskId, jobType, percentageComplete) - } - - actualTargetDiskType := pointer.From(disk.RecoveryTargetDiskAccountType) - actualTargetReplicaDiskType := pointer.From(disk.RecoveryReplicaDiskAccountType) - - mismatches := make([]string, 0) - if actualTargetDiskType != update.RecoveryTargetDiskType { - mismatches = append(mismatches, fmt.Sprintf("target_disk_type is %q, expected %q", actualTargetDiskType, update.RecoveryTargetDiskType)) - } - if actualTargetReplicaDiskType != update.RecoveryReplicaDiskType { - mismatches = append(mismatches, fmt.Sprintf("target_replica_disk_type is %q, expected %q", actualTargetReplicaDiskType, update.RecoveryReplicaDiskType)) - } - if len(mismatches) > 0 { - return fmt.Sprintf("%s: %s", update.DiskId, strings.Join(mismatches, ", ")) - } - - return "" -} - func normalizeSiteRecoveryReplicatedVMManagedDiskID(input string) string { if parsed, err := commonids.ParseManagedDiskIDInsensitively(input); err == nil { return strings.ToLower(parsed.ID()) From 105c0ba88fea044378b643261d2a250719a56fa9 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 5 Jun 2026 04:47:00 +0000 Subject: [PATCH 12/19] fix: enforce new disk type for Site Recovery replicated VMs --- .../site_recovery_replicated_vm_resource.go | 127 ++++++++++++++++-- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index f900c0629bb3..0a4f6df38499 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -46,10 +46,11 @@ import ( func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { return &pluginsdk.Resource{ - Create: resourceSiteRecoveryReplicatedItemCreate, - Read: resourceSiteRecoveryReplicatedItemRead, - Update: resourceSiteRecoveryReplicatedItemUpdate, - Delete: resourceSiteRecoveryReplicatedItemDelete, + Create: resourceSiteRecoveryReplicatedItemCreate, + Read: resourceSiteRecoveryReplicatedItemRead, + Update: resourceSiteRecoveryReplicatedItemUpdate, + Delete: resourceSiteRecoveryReplicatedItemDelete, + CustomizeDiff: pluginsdk.CustomizeDiffShim(resourceSiteRecoveryReplicatedVMCustomizeDiff), Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { _, err := parse.ReplicationProtectedItemID(id) return err @@ -207,21 +208,18 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { "disk_id": { Type: pluginsdk.TypeString, Required: true, - ForceNew: true, ValidateFunc: validation.StringIsNotEmpty, }, "staging_storage_account_id": { Type: pluginsdk.TypeString, Required: true, - ForceNew: true, ValidateFunc: commonids.ValidateStorageAccountID, }, "target_resource_group_id": { Type: pluginsdk.TypeString, Required: true, - ForceNew: true, ValidateFunc: commonids.ValidateResourceGroupID, }, @@ -240,7 +238,6 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { "target_disk_encryption_set_id": { Type: pluginsdk.TypeString, Optional: true, - ForceNew: true, ValidateFunc: commonids.ValidateDiskEncryptionSetID, }, @@ -416,6 +413,66 @@ func diskEncryptionResource() *pluginsdk.Resource { return args } +func resourceSiteRecoveryReplicatedVMCustomizeDiff(_ context.Context, d *pluginsdk.ResourceDiff, _ interface{}) error { + if !d.HasChange("managed_disk") { + return nil + } + + oldRaw, newRaw := d.GetChange("managed_disk") + oldDisks, ok := oldRaw.(*pluginsdk.Set) + if !ok || oldDisks.Len() == 0 { + return nil + } + + newDisks, ok := newRaw.(*pluginsdk.Set) + if !ok { + return nil + } + + if siteRecoveryReplicatedVMManagedDiskChangeRequiresNew(oldDisks.List(), newDisks.List()) { + return d.ForceNew("managed_disk") + } + + return nil +} + +func siteRecoveryReplicatedVMManagedDiskChangeRequiresNew(oldDisks, newDisks []interface{}) bool { + if len(oldDisks) != len(newDisks) { + return true + } + + newDisksById := make(map[string]map[string]interface{}, len(newDisks)) + for _, raw := range newDisks { + disk := raw.(map[string]interface{}) + newDisksById[strings.ToLower(siteRecoveryReplicatedVMManagedDiskValue(disk, "disk_id"))] = disk + } + + for _, raw := range oldDisks { + oldDisk := raw.(map[string]interface{}) + diskId := strings.ToLower(siteRecoveryReplicatedVMManagedDiskValue(oldDisk, "disk_id")) + newDisk, ok := newDisksById[diskId] + if !ok { + return true + } + + for _, field := range []string{"staging_storage_account_id", "target_resource_group_id", "target_disk_encryption_set_id"} { + if siteRecoveryReplicatedVMManagedDiskValue(oldDisk, field) != siteRecoveryReplicatedVMManagedDiskValue(newDisk, field) { + return true + } + } + } + + return false +} + +func siteRecoveryReplicatedVMManagedDiskValue(disk map[string]interface{}, field string) string { + if value, ok := disk[field].(string); ok { + return value + } + + return "" +} + func resourceSiteRecoveryReplicatedItemCreate(d *pluginsdk.ResourceData, meta interface{}) error { subscriptionId := meta.(*clients.Client).Account.SubscriptionId resGroup := d.Get("resource_group_name").(string) @@ -1013,14 +1070,64 @@ func resourceSiteRecoveryReplicatedVMDiskHash(v interface{}) int { var buf bytes.Buffer if m, ok := v.(map[string]interface{}); ok { - if v, ok := m["disk_id"]; ok { - buf.WriteString(strings.ToLower(v.(string))) + for _, field := range []string{"disk_id", "staging_storage_account_id", "target_resource_group_id"} { + if fieldValue, ok := m[field].(string); ok { + buf.WriteString(strings.ToLower(fieldValue)) + } + buf.WriteString("|") } + + for _, field := range []string{"target_disk_type", "target_replica_disk_type"} { + if fieldValue, ok := m[field].(string); ok { + buf.WriteString(fieldValue) + } + buf.WriteString("|") + } + + if fieldValue, ok := m["target_disk_encryption_set_id"].(string); ok { + buf.WriteString(strings.ToLower(fieldValue)) + } + buf.WriteString("|") + siteRecoveryReplicatedVMAppendTargetDiskEncryptionHash(&buf, m["target_disk_encryption"]) } return pluginsdk.HashString(buf.String()) } +func siteRecoveryReplicatedVMAppendTargetDiskEncryptionHash(buf *bytes.Buffer, input interface{}) { + encryptionList, ok := input.([]interface{}) + if !ok || len(encryptionList) == 0 { + return + } + + encryption, ok := encryptionList[0].(map[string]interface{}) + if !ok { + return + } + + siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf, encryption, "disk_encryption_key", "secret_url", "vault_id") + siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf, encryption, "key_encryption_key", "key_url", "vault_id") +} + +func siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf *bytes.Buffer, encryption map[string]interface{}, blockName, firstField, secondField string) { + keyList, ok := encryption[blockName].([]interface{}) + if !ok || len(keyList) == 0 { + return + } + + key, ok := keyList[0].(map[string]interface{}) + if !ok { + return + } + + for _, field := range []string{firstField, secondField} { + if fieldValue, ok := key[field].(string); ok { + buf.WriteString(fieldValue) + } + buf.WriteString("|") + } +} + func waitForReplicationToBeHealthy(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) (*replicationprotecteditems.ReplicationProtectedItem, error) { log.Printf("Waiting for Site Recover to replicate VM.") stateConf := &pluginsdk.StateChangeConf{ From d2d14fd2ed9d2351a7021934b8359bac1254e9d7 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:40:20 +1000 Subject: [PATCH 13/19] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../site_recovery_replicated_vm_resource.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 0a4f6df38499..2a2b54173ab4 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -420,7 +420,7 @@ func resourceSiteRecoveryReplicatedVMCustomizeDiff(_ context.Context, d *plugins oldRaw, newRaw := d.GetChange("managed_disk") oldDisks, ok := oldRaw.(*pluginsdk.Set) - if !ok || oldDisks.Len() == 0 { + if !ok { return nil } @@ -705,7 +705,10 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl }, } - diskTypeTargets := siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks) + var diskTypeTargets []custompollers.SiteRecoveryReplicatedVMDiskTypeUpdate + if d.HasChange("managed_disk") { + diskTypeTargets = siteRecoveryReplicatedVMManagedDiskTypeUpdateTargets(managedDisks) + } err = client.UpdateThenPoll(ctx, id, parameters) if err != nil { From c61a71aee505689c426b1dd2d266aaaa32b8c2d4 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:04:41 +0000 Subject: [PATCH 14/19] fix docs lint and address review feedback - document-lint: skip the managed_disk ForceNew fields whose ForceNew is enforced via CustomizeDiff (keyed by disk_id) rather than the schema flag; target_disk_type/target_replica_disk_type stay updatable in place, so schema-level ForceNew can't be used and the linter can't see it - normalize disk encryption key fields when hashing managed_disk - document why the disk type poller requires 10 consecutive matching reads --- .../site_recovery_replicated_vm_disk_type_poller.go | 5 +++++ .../recoveryservices/site_recovery_replicated_vm_resource.go | 2 +- internal/tools/document-lint/check/skip_config.go | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go index f7230cc7cd3b..52b8d12ef25f 100644 --- a/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go +++ b/internal/services/recoveryservices/custompollers/site_recovery_replicated_vm_disk_type_poller.go @@ -20,6 +20,11 @@ import ( var _ pollers.PollerType = &SiteRecoveryReplicatedVMDiskTypePoller{} +// siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads is intentionally high. The Site +// Recovery service is eventually consistent and can briefly report the requested disk type +// before the value flips back while replication catches up, so we require this many +// consecutive matching reads before treating the update as complete. Testing showed that a +// lower threshold (e.g. 5) still produced false positives. const siteRecoveryReplicatedVMDiskTypeRequiredSuccessfulReads = 10 type SiteRecoveryReplicatedVMDiskTypeUpdate struct { diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 2a2b54173ab4..059ca4cec3a4 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -1125,7 +1125,7 @@ func siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf *bytes.Buffer, encr for _, field := range []string{firstField, secondField} { if fieldValue, ok := key[field].(string); ok { - buf.WriteString(fieldValue) + buf.WriteString(strings.ToLower(fieldValue)) } buf.WriteString("|") } diff --git a/internal/tools/document-lint/check/skip_config.go b/internal/tools/document-lint/check/skip_config.go index 205cdcfaaad0..e2797c98de09 100644 --- a/internal/tools/document-lint/check/skip_config.go +++ b/internal/tools/document-lint/check/skip_config.go @@ -30,6 +30,9 @@ var skipProps = []string{ "/azurerm_orchestrated_virtual_machine_scale_set.os_profile.(windows|linux)_configuration.secret.certificate", "/azurerm_eventgrid_event_subscription.advanced_filter.*", "/azurerm_eventgrid_system_topic_event_subscription.advanced_filter.*", + + // managed_disk ForceNew is enforced in CustomizeDiff, not the schema flag + "/azurerm_site_recovery_replicated_vm.managed_disk.(disk_id|staging_storage_account_id|target_resource_group_id|target_disk_encryption_set_id)", } var skipConfig = &struct { From 923c44a8ad53c9d51b957e969037505bc831f869 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:15:30 +0000 Subject: [PATCH 15/19] update code --- .../recoveryservices/site_recovery_replicated_vm_resource.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 059ca4cec3a4..d00b98d3d890 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -418,6 +418,10 @@ func resourceSiteRecoveryReplicatedVMCustomizeDiff(_ context.Context, d *plugins return nil } + if !d.NewValueKnown("managed_disk") || d.Id() == "" { + return d.SetNewComputed("managed_disk") + } + oldRaw, newRaw := d.GetChange("managed_disk") oldDisks, ok := oldRaw.(*pluginsdk.Set) if !ok { From 972469f820bfca55bf8bd32b767b7ca394be6c38 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:46:12 +0000 Subject: [PATCH 16/19] update `managed_disk` to typeList --- .../migration/site_recovery_replicated_vm.go | 294 ++++++++++++++++++ .../site_recovery_replicated_vm_test.go | 61 ++++ .../site_recovery_replicated_vm_resource.go | 112 +++---- 3 files changed, 403 insertions(+), 64 deletions(-) create mode 100644 internal/services/recoveryservices/migration/site_recovery_replicated_vm.go create mode 100644 internal/services/recoveryservices/migration/site_recovery_replicated_vm_test.go diff --git a/internal/services/recoveryservices/migration/site_recovery_replicated_vm.go b/internal/services/recoveryservices/migration/site_recovery_replicated_vm.go new file mode 100644 index 000000000000..8619a89cf8b6 --- /dev/null +++ b/internal/services/recoveryservices/migration/site_recovery_replicated_vm.go @@ -0,0 +1,294 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + +package migration + +import ( + "context" + + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +var _ pluginsdk.StateUpgrade = SiteRecoveryReplicatedVMV0ToV1{} + +type SiteRecoveryReplicatedVMV0ToV1 struct{} + +func (SiteRecoveryReplicatedVMV0ToV1) UpgradeFunc() pluginsdk.StateUpgraderFunc { + return func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + return rawState, nil + } +} + +func (SiteRecoveryReplicatedVMV0ToV1) Schema() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "resource_group_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "recovery_vault_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "source_recovery_fabric_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "source_vm_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "target_recovery_fabric_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "recovery_replication_policy_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "source_recovery_protection_container_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "target_recovery_protection_container_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "target_resource_group_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "target_availability_set_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_zone": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_network_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + + "test_network_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + + "target_edge_zone": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "unmanaged_disk": { + Type: pluginsdk.TypeSet, + Optional: true, + Computed: true, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "disk_uri": { + Type: pluginsdk.TypeString, + Required: true, + }, + "staging_storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + "target_storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + }, + + "multi_vm_group_name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "managed_disk": { + Type: pluginsdk.TypeSet, + Optional: true, + Computed: true, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "disk_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + "staging_storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + "target_resource_group_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + "target_disk_type": { + Type: pluginsdk.TypeString, + Required: true, + }, + "target_replica_disk_type": { + Type: pluginsdk.TypeString, + Required: true, + }, + "target_disk_encryption_set_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + "target_disk_encryption": { + Type: pluginsdk.TypeList, + Optional: true, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + MaxItems: 1, + Elem: diskEncryptionResourceV0(), + }, + }, + }, + }, + + "target_proximity_placement_group_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_boot_diagnostic_storage_account_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_capacity_reservation_group_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_virtual_machine_scale_set_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "target_virtual_machine_size": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + + "network_interface": { + Type: pluginsdk.TypeSet, + Optional: true, + Computed: true, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + Elem: networkInterfaceResourceV0(), + }, + } +} + +func networkInterfaceResourceV0() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "source_network_interface_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + "failover_test_static_ip": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + "target_static_ip": { + Type: pluginsdk.TypeString, + Optional: true, + }, + "failover_test_subnet_name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + "target_subnet_name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + "failover_test_public_ip_address_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + "recovery_load_balancer_backend_address_pool_ids": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "recovery_public_ip_address_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + }, + } +} + +func diskEncryptionResourceV0() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "disk_encryption_key": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 1, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_url": { + Type: pluginsdk.TypeString, + Required: true, + }, + "vault_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + }, + "key_encryption_key": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + ConfigMode: pluginsdk.SchemaConfigModeAttr, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "key_url": { + Type: pluginsdk.TypeString, + Required: true, + }, + "vault_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/services/recoveryservices/migration/site_recovery_replicated_vm_test.go b/internal/services/recoveryservices/migration/site_recovery_replicated_vm_test.go new file mode 100644 index 000000000000..36e96de1e6d8 --- /dev/null +++ b/internal/services/recoveryservices/migration/site_recovery_replicated_vm_test.go @@ -0,0 +1,61 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + +package migration + +import ( + "context" + "reflect" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +func TestSiteRecoveryReplicatedVMV0ToV1(t *testing.T) { + // `managed_disk` switched from a TypeSet to a TypeList. Both are persisted as a JSON array, so the + // upgrade is a no-op on the value itself - the version bump is what re-interprets it as a list. The + // upgrade must preserve the disks (order and contents) so Read can re-align them to config order. + input := map[string]interface{}{ + "name": "repl-1", + "managed_disk": []interface{}{ + map[string]interface{}{ + "disk_id": "/subscriptions/s/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1", + "staging_storage_account_id": "/subscriptions/s/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa1", + "target_resource_group_id": "/subscriptions/s/resourceGroups/rg", + "target_disk_type": "Premium_LRS", + "target_replica_disk_type": "Premium_LRS", + }, + map[string]interface{}{ + "disk_id": "/subscriptions/s/resourceGroups/rg/providers/Microsoft.Compute/disks/disk2", + "staging_storage_account_id": "/subscriptions/s/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa1", + "target_resource_group_id": "/subscriptions/s/resourceGroups/rg", + "target_disk_type": "StandardSSD_LRS", + "target_replica_disk_type": "StandardSSD_LRS", + }, + }, + } + + expected := map[string]interface{}{ + "name": "repl-1", + "managed_disk": input["managed_disk"], + } + + got, err := SiteRecoveryReplicatedVMV0ToV1{}.UpgradeFunc()(context.Background(), input, nil) + if err != nil { + t.Fatalf("upgrading state: %v", err) + } + + if !reflect.DeepEqual(got, expected) { + t.Fatalf("unexpected upgraded state:\n got: %#v\nwant: %#v", got, expected) + } +} + +func TestSiteRecoveryReplicatedVMV0SchemaManagedDiskIsSet(t *testing.T) { + managedDisk, ok := SiteRecoveryReplicatedVMV0ToV1{}.Schema()["managed_disk"] + if !ok { + t.Fatal("expected managed_disk in the v0 schema") + } + if managedDisk.Type != pluginsdk.TypeSet { + t.Fatalf("v0 managed_disk must be a TypeSet, got %v", managedDisk.Type) + } +} diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index d00b98d3d890..60606af76487 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -4,10 +4,10 @@ package recoveryservices import ( - "bytes" "context" "fmt" "log" + "sort" "strings" "time" @@ -34,6 +34,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/custompollers" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/validate" resourceParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/parse" @@ -56,6 +57,11 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { return err }), + SchemaVersion: 1, + StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{ + 0: migration.SiteRecoveryReplicatedVMV0ToV1{}, + }), + Timeouts: &pluginsdk.ResourceTimeout{ Create: pluginsdk.DefaultTimeout(180 * time.Minute), Read: pluginsdk.DefaultTimeout(5 * time.Minute), @@ -198,11 +204,10 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { }, "managed_disk": { - Type: pluginsdk.TypeSet, + Type: pluginsdk.TypeList, Optional: true, Computed: true, ConfigMode: pluginsdk.SchemaConfigModeAttr, - Set: resourceSiteRecoveryReplicatedVMDiskHash, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "disk_id": { @@ -418,22 +423,22 @@ func resourceSiteRecoveryReplicatedVMCustomizeDiff(_ context.Context, d *plugins return nil } - if !d.NewValueKnown("managed_disk") || d.Id() == "" { - return d.SetNewComputed("managed_disk") - } - oldRaw, newRaw := d.GetChange("managed_disk") - oldDisks, ok := oldRaw.(*pluginsdk.Set) + oldDisks, ok := oldRaw.([]interface{}) if !ok { return nil } - newDisks, ok := newRaw.(*pluginsdk.Set) + newDisks, ok := newRaw.([]interface{}) if !ok { return nil } - if siteRecoveryReplicatedVMManagedDiskChangeRequiresNew(oldDisks.List(), newDisks.List()) { + // Changing a disk's identity (the disk itself, its staging account, target resource group or disk + // encryption set) requires the protected item to be recreated, whereas `target_disk_type` and + // `target_replica_disk_type` are migrated in place. Disks are matched by `disk_id` so that merely + // re-ordering the configured disks does not force a new resource. + if siteRecoveryReplicatedVMManagedDiskChangeRequiresNew(oldDisks, newDisks) { return d.ForceNew("managed_disk") } @@ -523,7 +528,7 @@ func resourceSiteRecoveryReplicatedItemCreate(d *pluginsdk.ResourceData, meta in } } - managedDisksGet := d.Get("managed_disk").(*pluginsdk.Set).List() + managedDisksGet := d.Get("managed_disk").([]interface{}) managedDisks := make([]replicationprotecteditems.A2AVMManagedDiskInputDetails, 0, len(managedDisksGet)) for _, raw := range managedDisksGet { diskInput := raw.(map[string]interface{}) @@ -653,7 +658,7 @@ func resourceSiteRecoveryReplicatedItemUpdateInternal(ctx context.Context, d *pl }) } - managedDisksGet := d.Get("managed_disk").(*pluginsdk.Set).List() + managedDisksGet := d.Get("managed_disk").([]interface{}) managedDisks := make([]replicationprotecteditems.A2AVMManagedDiskUpdateDetails, 0, len(managedDisksGet)) for _, raw := range managedDisksGet { diskInput := raw.(map[string]interface{}) @@ -999,7 +1004,9 @@ func resourceSiteRecoveryReplicatedItemRead(d *pluginsdk.ResourceData, meta inte disksOutput = append(disksOutput, diskOutput) } - d.Set("managed_disk", pluginsdk.NewSet(resourceSiteRecoveryReplicatedVMDiskHash, disksOutput)) + // The service does not guarantee the order of the returned disks, so re-order them to match + // the configured / prior-state order to avoid a spurious diff for this `TypeList`. + d.Set("managed_disk", siteRecoveryReplicatedVMOrderManagedDisks(d.Get("managed_disk").([]interface{}), disksOutput)) } if a2aDetails.VMNics != nil { @@ -1073,66 +1080,43 @@ func resourceSiteRecoveryReplicatedItemDelete(d *pluginsdk.ResourceData, meta in return nil } -func resourceSiteRecoveryReplicatedVMDiskHash(v interface{}) int { - var buf bytes.Buffer - - if m, ok := v.(map[string]interface{}); ok { - for _, field := range []string{"disk_id", "staging_storage_account_id", "target_resource_group_id"} { - if fieldValue, ok := m[field].(string); ok { - buf.WriteString(strings.ToLower(fieldValue)) - } - buf.WriteString("|") - } - - for _, field := range []string{"target_disk_type", "target_replica_disk_type"} { - if fieldValue, ok := m[field].(string); ok { - buf.WriteString(fieldValue) - } - buf.WriteString("|") - } - - if fieldValue, ok := m["target_disk_encryption_set_id"].(string); ok { - buf.WriteString(strings.ToLower(fieldValue)) +func siteRecoveryReplicatedVMOrderManagedDisks(configured []interface{}, apiDisks []interface{}) []interface{} { + remaining := make(map[string]interface{}, len(apiDisks)) + for _, raw := range apiDisks { + disk, ok := raw.(map[string]interface{}) + if !ok { + continue } - buf.WriteString("|") - siteRecoveryReplicatedVMAppendTargetDiskEncryptionHash(&buf, m["target_disk_encryption"]) + remaining[strings.ToLower(siteRecoveryReplicatedVMManagedDiskValue(disk, "disk_id"))] = raw } - return pluginsdk.HashString(buf.String()) -} - -func siteRecoveryReplicatedVMAppendTargetDiskEncryptionHash(buf *bytes.Buffer, input interface{}) { - encryptionList, ok := input.([]interface{}) - if !ok || len(encryptionList) == 0 { - return - } + ordered := make([]interface{}, 0, len(apiDisks)) - encryption, ok := encryptionList[0].(map[string]interface{}) - if !ok { - return + // Emit the disks in the order they appear in the configuration / prior state. + for _, raw := range configured { + disk, ok := raw.(map[string]interface{}) + if !ok { + continue + } + diskId := strings.ToLower(siteRecoveryReplicatedVMManagedDiskValue(disk, "disk_id")) + if apiDisk, ok := remaining[diskId]; ok { + ordered = append(ordered, apiDisk) + delete(remaining, diskId) + } } - siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf, encryption, "disk_encryption_key", "secret_url", "vault_id") - siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf, encryption, "key_encryption_key", "key_url", "vault_id") -} - -func siteRecoveryReplicatedVMAppendDiskEncryptionKeyHash(buf *bytes.Buffer, encryption map[string]interface{}, blockName, firstField, secondField string) { - keyList, ok := encryption[blockName].([]interface{}) - if !ok || len(keyList) == 0 { - return + // Append any disks that are not present in the configuration (e.g. on import) in a deterministic + // order so the result is stable. + leftoverIds := make([]string, 0, len(remaining)) + for diskId := range remaining { + leftoverIds = append(leftoverIds, diskId) } - - key, ok := keyList[0].(map[string]interface{}) - if !ok { - return + sort.Strings(leftoverIds) + for _, diskId := range leftoverIds { + ordered = append(ordered, remaining[diskId]) } - for _, field := range []string{firstField, secondField} { - if fieldValue, ok := key[field].(string); ok { - buf.WriteString(strings.ToLower(fieldValue)) - } - buf.WriteString("|") - } + return ordered } func waitForReplicationToBeHealthy(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) (*replicationprotecteditems.ReplicationProtectedItem, error) { From aeb7f6581d35949cabd4ded320c75edaed82b7b7 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:35:53 +0000 Subject: [PATCH 17/19] diffsuppress --- .../site_recovery_replicated_vm_resource.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 60606af76487..a03c408551bf 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -214,18 +214,24 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { Type: pluginsdk.TypeString, Required: true, ValidateFunc: validation.StringIsNotEmpty, + // The service returns IDs with inconsistent casing. + DiffSuppressFunc: suppress.CaseDifference, }, "staging_storage_account_id": { Type: pluginsdk.TypeString, Required: true, ValidateFunc: commonids.ValidateStorageAccountID, + // The service returns IDs with inconsistent casing. + DiffSuppressFunc: suppress.CaseDifference, }, "target_resource_group_id": { Type: pluginsdk.TypeString, Required: true, ValidateFunc: commonids.ValidateResourceGroupID, + // The service returns IDs with inconsistent casing. + DiffSuppressFunc: suppress.CaseDifference, }, "target_disk_type": { @@ -244,6 +250,8 @@ func resourceSiteRecoveryReplicatedVM() *pluginsdk.Resource { Type: pluginsdk.TypeString, Optional: true, ValidateFunc: commonids.ValidateDiskEncryptionSetID, + // The service returns IDs with inconsistent casing. + DiffSuppressFunc: suppress.CaseDifference, }, "target_disk_encryption": { From 603c2563f4f4809ee58a7ffa199211f98d77c12a Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:54:50 +0000 Subject: [PATCH 18/19] fix test --- .../recoveryservices/site_recovery_replicated_vm_resource.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index a03c408551bf..6368fdbed959 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -433,7 +433,8 @@ func resourceSiteRecoveryReplicatedVMCustomizeDiff(_ context.Context, d *plugins oldRaw, newRaw := d.GetChange("managed_disk") oldDisks, ok := oldRaw.([]interface{}) - if !ok { + + if !ok || len(oldDisks) == 0 { return nil } From ca5dde00a91a85e7055729838526a5c9e746808f Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:11:13 +0000 Subject: [PATCH 19/19] update per comment --- .../recoveryservices/site_recovery_replicated_vm_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go index 6368fdbed959..277c9c845030 100644 --- a/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go +++ b/internal/services/recoveryservices/site_recovery_replicated_vm_resource.go @@ -474,7 +474,7 @@ func siteRecoveryReplicatedVMManagedDiskChangeRequiresNew(oldDisks, newDisks []i } for _, field := range []string{"staging_storage_account_id", "target_resource_group_id", "target_disk_encryption_set_id"} { - if siteRecoveryReplicatedVMManagedDiskValue(oldDisk, field) != siteRecoveryReplicatedVMManagedDiskValue(newDisk, field) { + if !strings.EqualFold(siteRecoveryReplicatedVMManagedDiskValue(oldDisk, field), siteRecoveryReplicatedVMManagedDiskValue(newDisk, field)) { return true } }