Skip to content

Commit 99ab077

Browse files
authored
azurerm_postgresql_flexible_server_virtual_endpoint - is no longer removed from state when a fail-over occurs (#29424)
1 parent 5d935a6 commit 99ab077

4 files changed

Lines changed: 200 additions & 38 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package migration
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
8+
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresql/2024-08-01/servers"
9+
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresql/2024-08-01/virtualendpoints"
10+
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
11+
)
12+
13+
type PostgresqlFlexibleServerVirtualEndpointV0toV1 struct{}
14+
15+
var _ pluginsdk.StateUpgrade = PostgresqlFlexibleServerVirtualEndpointV0toV1{}
16+
17+
func (l PostgresqlFlexibleServerVirtualEndpointV0toV1) Schema() map[string]*pluginsdk.Schema {
18+
return map[string]*pluginsdk.Schema{
19+
"name": {
20+
Type: pluginsdk.TypeString,
21+
Required: true,
22+
},
23+
"source_server_id": {
24+
Type: pluginsdk.TypeString,
25+
Required: true,
26+
},
27+
"replica_server_id": {
28+
Type: pluginsdk.TypeString,
29+
Required: true,
30+
},
31+
"type": {
32+
Type: pluginsdk.TypeString,
33+
Required: true,
34+
},
35+
}
36+
}
37+
38+
func (l PostgresqlFlexibleServerVirtualEndpointV0toV1) UpgradeFunc() pluginsdk.StateUpgraderFunc {
39+
return func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
40+
oldId := rawState["id"].(string)
41+
name := rawState["name"].(string)
42+
43+
sourceServerId, err := servers.ParseFlexibleServerID(rawState["source_server_id"].(string))
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
sourceEndpointId := virtualendpoints.NewVirtualEndpointID(sourceServerId.SubscriptionId, sourceServerId.ResourceGroupName, sourceServerId.FlexibleServerName, name)
49+
50+
replicaServerId, err := servers.ParseFlexibleServerID(rawState["replica_server_id"].(string))
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
replicaEndpointId := virtualendpoints.NewVirtualEndpointID(replicaServerId.SubscriptionId, replicaServerId.ResourceGroupName, replicaServerId.FlexibleServerName, name)
56+
57+
newId := commonids.NewCompositeResourceID(&sourceEndpointId, &replicaEndpointId).ID()
58+
59+
log.Printf("[DEBUG] Updating ID from %q to %q", oldId, newId)
60+
61+
rawState["id"] = newId
62+
return rawState, nil
63+
}
64+
}

internal/services/postgres/postgresql_flexible_server_virtual_endpoint_resource.go

Lines changed: 121 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresql/2024-08-01/virtualendpoints"
1717
"github.com/hashicorp/terraform-provider-azurerm/internal/locks"
1818
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
19+
"github.com/hashicorp/terraform-provider-azurerm/internal/services/postgres/migration"
1920
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
2021
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation"
2122
)
@@ -31,6 +32,8 @@ type PostgresqlFlexibleServerVirtualEndpointModel struct {
3132

3233
var _ sdk.ResourceWithUpdate = PostgresqlFlexibleServerVirtualEndpointResource{}
3334

35+
var _ sdk.ResourceWithStateMigration = PostgresqlFlexibleServerVirtualEndpointResource{}
36+
3437
func (r PostgresqlFlexibleServerVirtualEndpointResource) ModelObject() interface{} {
3538
return &PostgresqlFlexibleServerVirtualEndpointModel{}
3639
}
@@ -40,13 +43,33 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) ResourceType() string {
4043
}
4144

4245
func (r PostgresqlFlexibleServerVirtualEndpointResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
43-
return virtualendpoints.ValidateVirtualEndpointID
46+
return func(input interface{}, key string) (warnings []string, errors []error) {
47+
v, ok := input.(string)
48+
if !ok {
49+
errors = append(errors, fmt.Errorf("expected %q to be a string", key))
50+
return
51+
}
52+
53+
if _, err := commonids.ParseCompositeResourceID(v, &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{}); err != nil {
54+
errors = append(errors, err)
55+
}
56+
return
57+
}
4458
}
4559

4660
func (r PostgresqlFlexibleServerVirtualEndpointResource) Attributes() map[string]*pluginsdk.Schema {
4761
return map[string]*pluginsdk.Schema{}
4862
}
4963

64+
func (r PostgresqlFlexibleServerVirtualEndpointResource) StateUpgraders() sdk.StateUpgradeData {
65+
return sdk.StateUpgradeData{
66+
SchemaVersion: 1,
67+
Upgraders: map[int]pluginsdk.StateUpgrade{
68+
0: migration.PostgresqlFlexibleServerVirtualEndpointV0toV1{},
69+
},
70+
}
71+
}
72+
5073
func (r PostgresqlFlexibleServerVirtualEndpointResource) Arguments() map[string]*pluginsdk.Schema {
5174
return map[string]*pluginsdk.Schema{
5275
"name": {
@@ -101,28 +124,31 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Create() sdk.ResourceFu
101124
return err
102125
}
103126

104-
id := virtualendpoints.NewVirtualEndpointID(sourceServerId.SubscriptionId, sourceServerId.ResourceGroupName, sourceServerId.FlexibleServerName, virtualEndpoint.Name)
127+
sourceEndpointId := virtualendpoints.NewVirtualEndpointID(sourceServerId.SubscriptionId, sourceServerId.ResourceGroupName, sourceServerId.FlexibleServerName, virtualEndpoint.Name)
128+
replicaEndpointId := virtualendpoints.NewVirtualEndpointID(replicaServerId.SubscriptionId, replicaServerId.ResourceGroupName, replicaServerId.FlexibleServerName, virtualEndpoint.Name)
105129

106-
locks.ByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
107-
defer locks.UnlockByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
130+
locks.ByName(sourceEndpointId.FlexibleServerName, postgresqlFlexibleServerResourceName)
131+
defer locks.UnlockByName(sourceEndpointId.FlexibleServerName, postgresqlFlexibleServerResourceName)
108132

109-
if replicaServerId.FlexibleServerName != id.FlexibleServerName {
133+
if replicaServerId.FlexibleServerName != replicaEndpointId.FlexibleServerName {
110134
locks.ByName(replicaServerId.FlexibleServerName, postgresqlFlexibleServerResourceName)
111135
defer locks.UnlockByName(replicaServerId.FlexibleServerName, postgresqlFlexibleServerResourceName)
112136
}
113137

114138
// This API can be a bit flaky if the same named resource is created/destroyed quickly
115139
// usually waiting a minute or two before redeploying is enough to resolve the conflict
116-
if err = client.CreateThenPoll(ctx, id, virtualendpoints.VirtualEndpointResource{
140+
if err = client.CreateThenPoll(ctx, sourceEndpointId, virtualendpoints.VirtualEndpointResource{
117141
Name: &virtualEndpoint.Name,
118142
Properties: &virtualendpoints.VirtualEndpointResourceProperties{
119143
EndpointType: pointer.To(virtualendpoints.VirtualEndpointType(virtualEndpoint.Type)),
120144
Members: &[]string{replicaServerId.FlexibleServerName},
121145
},
122146
}); err != nil {
123-
return fmt.Errorf("creating %s: %+v", id, err)
147+
return fmt.Errorf("creating %s: %+v", sourceEndpointId, err)
124148
}
125149

150+
id := commonids.NewCompositeResourceID(&sourceEndpointId, &replicaEndpointId)
151+
126152
metadata.SetID(id)
127153

128154
return nil
@@ -139,40 +165,55 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Read() sdk.ResourceFunc
139165

140166
state := PostgresqlFlexibleServerVirtualEndpointModel{}
141167

142-
id, err := virtualendpoints.ParseVirtualEndpointID(metadata.ResourceData.Id())
168+
id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{})
143169
if err != nil {
144170
return err
145171
}
146172

147-
resp, err := client.Get(ctx, *id)
173+
// In case of a fail-over, we need to see if the endpoint lives under the source id or the replica id
174+
failOverHasOccurred := false
175+
virtualEndpointId := *id.First
176+
resp, err := client.Get(ctx, virtualEndpointId)
148177
if err != nil {
149178
if response.WasNotFound(resp.HttpResponse) {
150-
log.Printf("[INFO] %s does not exist - removing from state", metadata.ResourceData.Id())
151-
return metadata.MarkAsGone(id)
179+
virtualEndpointId = *id.Second
180+
// if the endpoint doesn't exist under the source server, look for it under the replica server
181+
resp, err = client.Get(ctx, virtualEndpointId)
182+
if err != nil {
183+
if response.WasNotFound(resp.HttpResponse) {
184+
// the endpoint was not found under the source or the replica server so it can safely be removed from state
185+
log.Printf("[INFO] %s does not exist - removing from state", metadata.ResourceData.Id())
186+
return metadata.MarkAsGone(id)
187+
}
188+
return fmt.Errorf("retrieving %s: %+v", id, err)
189+
}
190+
failOverHasOccurred = true
191+
}
192+
// if we errored and didn't find the endpoint under the replica id, then we error here
193+
if !failOverHasOccurred {
194+
return fmt.Errorf("retrieving %s: %+v", id, err)
152195
}
153-
return fmt.Errorf("retrieving %s: %+v", id, err)
154196
}
155197

156-
state.Name = id.VirtualEndpointName
198+
state.Name = virtualEndpointId.VirtualEndpointName
157199

158200
if model := resp.Model; model != nil {
159201
if props := model.Properties; props != nil {
160202
state.Type = string(pointer.From(props.EndpointType))
161203

162204
if props.Members == nil || len(*props.Members) == 0 {
163205
// if members list is nil or empty, this is an endpoint that was previously deleted
164-
log.Printf("[INFO] Postgresql Flexible Server Endpoint %q was previously deleted - removing from state", id.ID())
206+
log.Printf("[INFO] Postgresql Flexible Server Endpoint %q was previously deleted - removing from state", id.First.ID())
165207
return metadata.MarkAsGone(id)
166208
}
167209

168-
state.SourceServerId = servers.NewFlexibleServerID(id.SubscriptionId, id.ResourceGroupName, (*props.Members)[0]).ID()
169-
210+
state.SourceServerId = servers.NewFlexibleServerID(id.First.SubscriptionId, id.First.ResourceGroupName, (*props.Members)[0]).ID()
170211
// Model.Properties.Members can contain 1 member which means source and replica are identical, or it can contain
171212
// 2 members when source and replica are different => [source_server_id, replication_server_name]
172-
replicaServerId := servers.NewFlexibleServerID(id.SubscriptionId, id.ResourceGroupName, (*props.Members)[0]).ID()
213+
replicaServerId := state.SourceServerId
173214

174215
if len(*props.Members) == 2 {
175-
replicaServer, err := lookupFlexibleServerByName(ctx, flexibleServerClient, id, (*props.Members)[1], state.SourceServerId)
216+
replicaServer, err := lookupFlexibleServerByName(ctx, flexibleServerClient, virtualEndpointId, (*props.Members)[1], state.SourceServerId)
176217
if err != nil {
177218
return err
178219
}
@@ -191,6 +232,11 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Read() sdk.ResourceFunc
191232
}
192233
}
193234

235+
// if a fail-over has occurred, the source/replica ids have swapped so we'll have to swap them back in Terraform to prevent a diff
236+
if failOverHasOccurred {
237+
state.SourceServerId, state.ReplicaServerId = state.ReplicaServerId, state.SourceServerId
238+
}
239+
194240
return metadata.Encode(&state)
195241
},
196242
}
@@ -202,15 +248,39 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Delete() sdk.ResourceFu
202248
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
203249
client := metadata.Client.Postgres.VirtualEndpointClient
204250

205-
id, err := virtualendpoints.ParseVirtualEndpointID(metadata.ResourceData.Id())
251+
id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{})
206252
if err != nil {
207253
return err
208254
}
209255

210-
locks.ByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
211-
defer locks.UnlockByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
256+
// In case of a fail-over, we need to see if the endpoint lives under the source id or the replica id before deleting
257+
failOverHasOccurred := false
258+
virtualEndpointId := *id.First
259+
resp, err := client.Get(ctx, virtualEndpointId)
260+
if err != nil {
261+
if response.WasNotFound(resp.HttpResponse) {
262+
virtualEndpointId = *id.Second
263+
// if the endpoint doesn't exist under the source server, look for it under the replica server
264+
resp, err = client.Get(ctx, virtualEndpointId)
265+
if err != nil {
266+
if response.WasNotFound(resp.HttpResponse) {
267+
// the endpoint was not found under the source or the replica server so we can exit here
268+
return nil
269+
}
270+
return fmt.Errorf("retrieving %s: %+v", id, err)
271+
}
272+
failOverHasOccurred = true
273+
}
274+
// if we errored and didn't find the endpoint under the replica id, then we error here
275+
if !failOverHasOccurred {
276+
return fmt.Errorf("retrieving %s: %+v", id, err)
277+
}
278+
}
212279

213-
if err := client.DeleteThenPoll(ctx, *id); err != nil {
280+
locks.ByName(virtualEndpointId.FlexibleServerName, postgresqlFlexibleServerResourceName)
281+
defer locks.UnlockByName(virtualEndpointId.FlexibleServerName, postgresqlFlexibleServerResourceName)
282+
283+
if err := client.DeleteThenPoll(ctx, virtualEndpointId); err != nil {
214284
return fmt.Errorf("deleting %s: %+v", *id, err)
215285
}
216286

@@ -221,7 +291,7 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Delete() sdk.ResourceFu
221291

222292
func (r PostgresqlFlexibleServerVirtualEndpointResource) Update() sdk.ResourceFunc {
223293
return sdk.ResourceFunc{
224-
Timeout: 5 * time.Minute,
294+
Timeout: 10 * time.Minute,
225295
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
226296
var virtualEndpoint PostgresqlFlexibleServerVirtualEndpointModel
227297
client := metadata.Client.Postgres.VirtualEndpointClient
@@ -230,25 +300,42 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Update() sdk.ResourceFu
230300
return err
231301
}
232302

233-
id, err := virtualendpoints.ParseVirtualEndpointID(metadata.ResourceData.Id())
303+
id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{})
234304
if err != nil {
235305
return err
236306
}
237307

308+
// attempt to retrieve the endpoint and see if a fail-over has occurred, if so error as we shouldn't update to a different replica server with the `source_server_id` and the `replica_server_id` being swapped
309+
virtualEndpointId := *id.First
310+
resp, err := client.Get(ctx, virtualEndpointId)
311+
if err != nil {
312+
if response.WasNotFound(resp.HttpResponse) {
313+
virtualEndpointId = virtualendpoints.NewVirtualEndpointID(id.Second.SubscriptionId, id.Second.ResourceGroupName, id.Second.FlexibleServerName, id.Second.VirtualEndpointName)
314+
// if the endpoint doesn't exist under the source server, look for it under the replica server
315+
_, err = client.Get(ctx, virtualEndpointId)
316+
if err != nil {
317+
return fmt.Errorf("retrieving %s: %+v", virtualEndpointId, err)
318+
}
319+
return fmt.Errorf("a fail-over has occurred and the `source_server_id` in the config is no longer the SourceServerId for the virtual endpoint. If you wish to change the `replica_server_id`, remove this resource from state and reimport it back in with the `replica_server_id` and `source_server_id` swapped")
320+
}
321+
return fmt.Errorf("retrieving %s: %+v", virtualEndpointId, err)
322+
}
323+
238324
replicaServerId, err := servers.ParseFlexibleServerID(virtualEndpoint.ReplicaServerId)
239325
if err != nil {
240326
return err
241327
}
242328

243-
locks.ByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
244-
defer locks.UnlockByName(id.FlexibleServerName, postgresqlFlexibleServerResourceName)
329+
locks.ByName(id.First.FlexibleServerName, postgresqlFlexibleServerResourceName)
330+
defer locks.UnlockByName(id.First.FlexibleServerName, postgresqlFlexibleServerResourceName)
245331

246-
if replicaServerId.FlexibleServerName != id.FlexibleServerName {
332+
if replicaServerId.FlexibleServerName != id.First.FlexibleServerName {
247333
locks.ByName(replicaServerId.FlexibleServerName, postgresqlFlexibleServerResourceName)
248334
defer locks.UnlockByName(replicaServerId.FlexibleServerName, postgresqlFlexibleServerResourceName)
249335
}
250336

251-
if err := client.UpdateThenPoll(ctx, *id, virtualendpoints.VirtualEndpointResourceForPatch{
337+
endpointId := virtualendpoints.NewVirtualEndpointID(id.First.SubscriptionId, id.First.ResourceGroupName, id.First.FlexibleServerName, virtualEndpoint.Name)
338+
if err := client.UpdateThenPoll(ctx, endpointId, virtualendpoints.VirtualEndpointResourceForPatch{
252339
Properties: &virtualendpoints.VirtualEndpointResourceProperties{
253340
EndpointType: pointer.To(virtualendpoints.VirtualEndpointType(virtualEndpoint.Type)),
254341
Members: pointer.To([]string{replicaServerId.FlexibleServerName}),
@@ -257,14 +344,19 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Update() sdk.ResourceFu
257344
return fmt.Errorf("updating %q: %+v", id, err)
258345
}
259346

347+
// the id has changed and needs to be updated
348+
replicaEndpointId := virtualendpoints.NewVirtualEndpointID(replicaServerId.SubscriptionId, replicaServerId.ResourceGroupName, replicaServerId.FlexibleServerName, virtualEndpoint.Name)
349+
endPointId := commonids.NewCompositeResourceID(&virtualEndpointId, &replicaEndpointId)
350+
metadata.SetID(endPointId)
351+
260352
return nil
261353
},
262354
}
263355
}
264356

265357
// The flexible endpoint API does not store the location/rg information on replicas it only stores the name.
266358
// This lookup is safe because replicas for a given source server are *not* allowed to have identical names
267-
func lookupFlexibleServerByName(ctx context.Context, flexibleServerClient *servers.ServersClient, virtualEndpointId *virtualendpoints.VirtualEndpointId, replicaServerName string, sourceServerId string) (*servers.Server, error) {
359+
func lookupFlexibleServerByName(ctx context.Context, flexibleServerClient *servers.ServersClient, virtualEndpointId virtualendpoints.VirtualEndpointId, replicaServerName string, sourceServerId string) (*servers.Server, error) {
268360
postgresServers, err := flexibleServerClient.ListCompleteMatchingPredicate(ctx, commonids.NewSubscriptionID(virtualEndpointId.SubscriptionId), servers.ServerOperationPredicate{
269361
Name: &replicaServerName,
270362
})

internal/services/postgres/postgresql_flexible_server_virtual_endpoint_resource_test.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/hashicorp/go-azure-helpers/lang/pointer"
12+
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
1213
"github.com/hashicorp/go-azure-sdk/resource-manager/postgresql/2024-08-01/virtualendpoints"
1314
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
1415
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
@@ -95,12 +96,14 @@ func TestAccPostgresqlFlexibleServerVirtualEndpoint_identicalSourceAndReplica(t
9596
}
9697

9798
func (r PostgresqlFlexibleServerVirtualEndpointResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
98-
id, err := virtualendpoints.ParseVirtualEndpointID(state.ID)
99+
id, err := commonids.ParseCompositeResourceID(state.ID, &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{})
99100
if err != nil {
100101
return nil, err
101102
}
102103

103-
resp, err := clients.Postgres.VirtualEndpointClient.Get(ctx, *id)
104+
virtualEndpointId := virtualendpoints.NewVirtualEndpointID(id.First.SubscriptionId, id.First.ResourceGroupName, id.First.FlexibleServerName, id.First.VirtualEndpointName)
105+
106+
resp, err := clients.Postgres.VirtualEndpointClient.Get(ctx, virtualEndpointId)
104107
if err != nil {
105108
return nil, fmt.Errorf("retrieving %s: %+v", id, err)
106109
}
@@ -109,12 +112,14 @@ func (r PostgresqlFlexibleServerVirtualEndpointResource) Exists(ctx context.Cont
109112
}
110113

111114
func (r PostgresqlFlexibleServerVirtualEndpointResource) Destroy(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
112-
id, err := virtualendpoints.ParseVirtualEndpointID(state.ID)
115+
id, err := commonids.ParseCompositeResourceID(state.ID, &virtualendpoints.VirtualEndpointId{}, &virtualendpoints.VirtualEndpointId{})
113116
if err != nil {
114117
return nil, err
115118
}
116119

117-
if err := client.Postgres.VirtualEndpointClient.DeleteThenPoll(ctx, *id); err != nil {
120+
virtualEndpointId := virtualendpoints.NewVirtualEndpointID(id.First.SubscriptionId, id.First.ResourceGroupName, id.First.FlexibleServerName, id.First.VirtualEndpointName)
121+
122+
if err := client.Postgres.VirtualEndpointClient.DeleteThenPoll(ctx, virtualEndpointId); err != nil {
118123
return nil, fmt.Errorf("deleting %s: %+v", *id, err)
119124
}
120125

0 commit comments

Comments
 (0)