Skip to content

Commit 55dffa1

Browse files
authored
fix(rdb): fix instance and read replica endpoints by enforcing IP config choice (#2400)
1 parent b20d2b2 commit 55dffa1

19 files changed

+8186
-6050
lines changed

docs/resources/rdb_instance.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ resource "scaleway_rdb_instance" "main" {
7979
private_network {
8080
pn_id = scaleway_vpc_private_network.pn.id
8181
ip_net = "172.16.20.4/22" # IP address within a given IP network
82-
(enable_ipam = false)
82+
# enable_ipam = false
8383
}
8484
}
8585
```
@@ -94,7 +94,7 @@ resource "scaleway_rdb_instance" "main" {
9494
engine = "PostgreSQL-11"
9595
private_network {
9696
pn_id = scaleway_vpc_private_network.pn.id
97-
(enable_ipam = true)
97+
enable_ipam = true
9898
}
9999
load_balancer {}
100100
}
@@ -179,8 +179,8 @@ Please consult the [GoDoc](https://pkg.go.dev/github.com/scaleway/scaleway-sdk-g
179179

180180
- `pn_id` - (Required) The ID of the private network.
181181
- `enable_ipam` - (Optional) Whether the endpoint should be configured with IPAM. Defaults to `false` if `ip_net` is defined, `true` otherwise.
182-
- `ip_net` - (Optional) The IP network address within the private subnet. This must be an IPv4 address with a CIDR notation.
183-
The IP network address within the private subnet is determined by the IP Address Management (IPAM) service if not set.
182+
- `ip_net` - (Optional) The IP network address within the private subnet. This must be an IPv4 address with a CIDR notation. If not set, The IP network address within the private subnet is determined by the IP Address Management (IPAM) service.
183+
- `enable_ipam` - (Optional) If true, the IP network address within the private subnet is determined by the IP Address Management (IPAM) service.
184184

185185
~> **NOTE:** Please calculate your host IP using [cidrhost](https://developer.hashicorp.com/terraform/language/functions/cidrhost). Otherwise, let IPAM service
186186
handle the host IP on the network.

docs/resources/rdb_read_replica.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ resource scaleway_rdb_read_replica "replica" {
3030
}
3131
```
3232

33-
### Private network
33+
### Private network with static endpoint
3434

3535
```terraform
3636
resource "scaleway_rdb_instance" "instance" {
@@ -49,7 +49,32 @@ resource "scaleway_rdb_read_replica" "replica" {
4949
instance_id = scaleway_rdb_instance.instance.id
5050
private_network {
5151
private_network_id = scaleway_vpc_private_network.pn.id
52-
service_ip = "192.168.1.254/24" // omit this attribute if private IP is determined by the IP Address Management (IPAM)
52+
service_ip = "192.168.1.254/24"
53+
# enable_ipam = false
54+
}
55+
}
56+
```
57+
58+
### Private network with IPAM
59+
60+
```terraform
61+
resource "scaleway_rdb_instance" "instance" {
62+
name = "rdb_instance"
63+
node_type = "db-dev-s"
64+
engine = "PostgreSQL-14"
65+
is_ha_cluster = false
66+
disable_backup = true
67+
user_name = "my_initial_user"
68+
password = "thiZ_is_v&ry_s3cret"
69+
}
70+
71+
resource "scaleway_vpc_private_network" "pn" {}
72+
73+
resource "scaleway_rdb_read_replica" "replica" {
74+
instance_id = scaleway_rdb_instance.instance.id
75+
private_network {
76+
private_network_id = scaleway_vpc_private_network.pn.id
77+
enable_ipam = true
5378
}
5479
}
5580
```
@@ -66,9 +91,8 @@ The following arguments are supported:
6691

6792
- `private_network` - (Optional) Create an endpoint in a private network.
6893
- `private_network_id` - (Required) UUID of the private network to be connected to the read replica.
69-
- `service_ip` - (Optional) The IP network address within the private subnet. This must be an IPv4 address with a
70-
CIDR notation. The IP network address within the private subnet is determined by the IP Address Management (IPAM)
71-
service if not set.
94+
- `service_ip` - (Optional) The IP network address within the private subnet. This must be an IPv4 address with a CIDR notation. If not set, The IP network address within the private subnet is determined by the IP Address Management (IPAM) service.
95+
- `enable_ipam` - (Optional) If true, the IP network address within the private subnet is determined by the IP Address Management (IPAM) service.
7296

7397
- `same_zone` - (Defaults to `true`) Defines whether to create the replica in the same availability zone as the main instance nodes or not.
7498

@@ -96,6 +120,7 @@ they are of the form `{region}/{id}`, e.g. `fr-par/11111111-1111-1111-1111-11111
96120
- `port` - TCP port of the endpoint.
97121
- `name` - Name of the endpoint.
98122
- `hostname` - Hostname of the endpoint. Only one of ip and hostname may be set.
123+
- `enable_ipam` - Indicates whether the IP is managed by IPAM.
99124

100125
## Import
101126

scaleway/data_source_ipam_ip_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func TestAccScalewayDataSourceIPAMIP_RDB(t *testing.T) {
151151
volume_size_in_gb = 10
152152
private_network {
153153
pn_id = "${scaleway_vpc_private_network.main.id}"
154+
enable_ipam = true
154155
}
155156
}
156157

scaleway/helpers_rdb.go

Lines changed: 92 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,35 +110,42 @@ func waitForRDBReadReplica(ctx context.Context, api *rdb.API, region scw.Region,
110110
}, scw.WithContext(ctx))
111111
}
112112

113-
func expandPrivateNetwork(data interface{}, exist bool, enableIpam bool) ([]*rdb.EndpointSpec, error) {
113+
func expandPrivateNetwork(data interface{}, exist bool, ipamConfig *bool, staticConfig *string) ([]*rdb.EndpointSpec, diag.Diagnostics) {
114114
if data == nil || !exist {
115115
return nil, nil
116116
}
117+
var diags diag.Diagnostics
117118

118119
res := make([]*rdb.EndpointSpec, 0, len(data.([]interface{})))
119120
for _, pn := range data.([]interface{}) {
120121
r := pn.(map[string]interface{})
121122
spec := &rdb.EndpointSpec{
122123
PrivateNetwork: &rdb.EndpointSpecPrivateNetwork{
123124
PrivateNetworkID: expandID(r["pn_id"].(string)),
125+
IpamConfig: &rdb.EndpointSpecPrivateNetworkIpamConfig{},
124126
},
125127
}
126-
if enableIpam {
127-
spec.PrivateNetwork.IpamConfig = &rdb.EndpointSpecPrivateNetworkIpamConfig{}
128-
} else {
129-
ipNet := r["ip_net"].(string)
130-
if len(ipNet) > 0 {
131-
ip, err := expandIPNet(r["ip_net"].(string))
132-
if err != nil {
133-
return res, err
134-
}
135-
spec.PrivateNetwork.ServiceIP = &ip
128+
129+
if staticConfig != nil {
130+
ip, err := expandIPNet(*staticConfig)
131+
if err != nil {
132+
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network ip_net (%s): %s", r["ip_net"], err))...)
136133
}
134+
spec.PrivateNetwork.ServiceIP = &ip
135+
spec.PrivateNetwork.IpamConfig = nil
136+
if ipamConfig != nil && *ipamConfig {
137+
diags = append(diags, diag.Diagnostic{
138+
Severity: diag.Warning,
139+
Detail: "`ip_net` field is set so `enable_ipam` field will be ignored",
140+
})
141+
}
142+
} else if ipamConfig == nil || !*ipamConfig {
143+
return nil, diag.FromErr(errors.New("at least one of `ip_net` or `enable_ipam` (set to true) must be set"))
137144
}
138145
res = append(res, spec)
139146
}
140147

141-
return res, nil
148+
return res, diags
142149
}
143150

144151
func expandLoadBalancer() *rdb.EndpointSpec {
@@ -221,34 +228,41 @@ func expandReadReplicaEndpointsSpecDirectAccess(data interface{}) *rdb.ReadRepli
221228
}
222229

223230
// expandReadReplicaEndpointsSpecPrivateNetwork expand read-replica private network endpoints from schema to specs
224-
func expandReadReplicaEndpointsSpecPrivateNetwork(data interface{}, enableIpam bool) (*rdb.ReadReplicaEndpointSpec, error) {
231+
func expandReadReplicaEndpointsSpecPrivateNetwork(data interface{}, ipamConfig *bool, staticConfig *string) (*rdb.ReadReplicaEndpointSpec, diag.Diagnostics) {
225232
if data == nil || len(data.([]interface{})) == 0 {
226233
return nil, nil
227234
}
228235
// private_network is a list of size 1
229236
data = data.([]interface{})[0]
230237

231238
rawEndpoint := data.(map[string]interface{})
239+
var diags diag.Diagnostics
232240

233-
endpoint := new(rdb.ReadReplicaEndpointSpec)
234-
endpoint.PrivateNetwork = &rdb.ReadReplicaEndpointSpecPrivateNetwork{
235-
PrivateNetworkID: expandID(rawEndpoint["private_network_id"]),
241+
endpoint := &rdb.ReadReplicaEndpointSpec{
242+
PrivateNetwork: &rdb.ReadReplicaEndpointSpecPrivateNetwork{
243+
PrivateNetworkID: expandID(rawEndpoint["private_network_id"]),
244+
IpamConfig: &rdb.ReadReplicaEndpointSpecPrivateNetworkIpamConfig{},
245+
},
236246
}
237247

238-
if enableIpam {
239-
endpoint.PrivateNetwork.IpamConfig = &rdb.ReadReplicaEndpointSpecPrivateNetworkIpamConfig{}
240-
} else {
241-
serviceIP := rawEndpoint["service_ip"].(string)
242-
if len(serviceIP) > 0 {
243-
ipNet, err := expandIPNet(serviceIP)
244-
if err != nil {
245-
return nil, fmt.Errorf("failed to parse private_network service_ip (%s): %w", rawEndpoint["service_ip"], err)
246-
}
247-
endpoint.PrivateNetwork.ServiceIP = &ipNet
248+
if staticConfig != nil {
249+
ipNet, err := expandIPNet(*staticConfig)
250+
if err != nil {
251+
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network service_ip (%s): %s", rawEndpoint["service_ip"], err))...)
252+
}
253+
endpoint.PrivateNetwork.ServiceIP = &ipNet
254+
endpoint.PrivateNetwork.IpamConfig = nil
255+
if ipamConfig != nil && !*ipamConfig {
256+
diags = append(diags, diag.Diagnostic{
257+
Severity: diag.Warning,
258+
Detail: "`service_ip` field is set so `enable_ipam` field will be ignored",
259+
})
248260
}
261+
} else if ipamConfig == nil || !*ipamConfig {
262+
return nil, diag.FromErr(errors.New("at least one of `service_ip` or `enable_ipam` (set to true) must be set"))
249263
}
250264

251-
return endpoint, nil
265+
return endpoint, diags
252266
}
253267

254268
// flattenReadReplicaEndpoints flatten read-replica endpoints to directAccess and privateNetwork
@@ -326,33 +340,76 @@ func rdbPrivilegeUpgradeV1SchemaType() cty.Type {
326340
})
327341
}
328342

329-
func isIpamEndpoint(resource interface{}, meta interface{}) (bool, error) {
343+
func getIPConfigCreate(d *schema.ResourceData, ipFieldName string) (ipamConfig *bool, staticConfig *string) {
344+
enableIpam, enableIpamSet := d.GetOk("private_network.0.enable_ipam")
345+
if enableIpamSet {
346+
ipamConfig = expandBoolPtr(enableIpam)
347+
}
348+
customIP, customIPSet := d.GetOk("private_network.0." + ipFieldName)
349+
if customIPSet {
350+
staticConfig = expandStringPtr(customIP)
351+
}
352+
return ipamConfig, staticConfig
353+
}
354+
355+
// getIPConfigUpdate forces the provider to read the user's config instead of checking the state, because "enable_ipam" is not readable from the API
356+
func getIPConfigUpdate(d *schema.ResourceData, ipFieldName string) (ipamConfig *bool, staticConfig *string) {
357+
if rawConfig := d.GetRawConfig(); !rawConfig.IsNull() {
358+
pnRawConfig := rawConfig.AsValueMap()["private_network"].AsValueSlice()[0].AsValueMap()
359+
if !pnRawConfig["enable_ipam"].IsNull() {
360+
if pnRawConfig["enable_ipam"].False() {
361+
ipamConfig = scw.BoolPtr(false)
362+
} else {
363+
ipamConfig = scw.BoolPtr(true)
364+
}
365+
}
366+
if !pnRawConfig[ipFieldName].IsNull() {
367+
value := pnRawConfig[ipFieldName].AsString()
368+
staticConfig = &value
369+
}
370+
}
371+
return ipamConfig, staticConfig
372+
}
373+
374+
func getIPAMConfigRead(resource interface{}, meta interface{}) (bool, error) {
330375
ipamAPI := ipam.NewAPI(meta.(*Meta).scwClient)
331376
request := &ipam.ListIPsRequest{
332377
ResourceType: "rdb_instance",
333378
IsIPv6: scw.BoolPtr(false),
334379
}
380+
var privateEndpoint *rdb.EndpointPrivateNetworkDetails
335381

336382
switch res := resource.(type) {
337383
case *rdb.Instance:
338384
request.Region = res.Region
339385
request.ResourceID = &res.ID
386+
for _, e := range res.Endpoints {
387+
if e.PrivateNetwork != nil {
388+
privateEndpoint = e.PrivateNetwork
389+
}
390+
}
340391
case *rdb.ReadReplica:
341392
request.Region = res.Region
342393
request.ResourceID = &res.InstanceID
394+
for _, e := range res.Endpoints {
395+
if e.PrivateNetwork != nil {
396+
privateEndpoint = e.PrivateNetwork
397+
}
398+
}
399+
}
400+
if privateEndpoint == nil {
401+
return false, nil
343402
}
344403

345404
ips, err := ipamAPI.ListIPs(request, scw.WithAllPages())
346405
if err != nil {
347406
return false, fmt.Errorf("could not list IPs: %w", err)
348407
}
349408

350-
switch ips.TotalCount {
351-
case 1:
352-
return true, nil
353-
case 0:
354-
return false, nil
355-
default:
356-
return false, fmt.Errorf("expected no more than 1 IP for instance, got %d", ips.TotalCount)
409+
for _, ip := range ips.IPs {
410+
if ip.Address.String() == privateEndpoint.ServiceIP.String() {
411+
return true, nil
412+
}
357413
}
414+
return false, nil
358415
}

scaleway/resource_rdb_instance.go

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88

9+
"github.com/hashicorp/terraform-plugin-log/tflog"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1011
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -150,12 +151,6 @@ func resourceScalewayRdbInstance() *schema.Resource {
150151
DiffSuppressFunc: diffSuppressFuncLocality,
151152
Description: "The private network ID",
152153
},
153-
"enable_ipam": {
154-
Type: schema.TypeBool,
155-
Optional: true,
156-
Computed: true,
157-
Description: "Whether or not the private network endpoint should be configured with IPAM",
158-
},
159154
// Computed
160155
"endpoint_id": {
161156
Type: schema.TypeString,
@@ -191,6 +186,12 @@ func resourceScalewayRdbInstance() *schema.Resource {
191186
Computed: true,
192187
Description: "The hostname of your endpoint",
193188
},
189+
"enable_ipam": {
190+
Type: schema.TypeBool,
191+
Optional: true,
192+
Computed: true,
193+
Description: "Whether or not the private network endpoint should be configured with IPAM",
194+
},
194195
"zone": zoneSchema(),
195196
},
196197
},
@@ -312,13 +313,14 @@ func resourceScalewayRdbInstanceCreate(ctx context.Context, d *schema.ResourceDa
312313

313314
// Init Endpoints
314315
if pn, pnExist := d.GetOk("private_network"); pnExist {
315-
enableIpam := true
316-
if _, ipNetSet := d.GetOk("private_network.0.ip_net"); ipNetSet {
317-
enableIpam = false
316+
ipamConfig, staticConfig := getIPConfigCreate(d, "ip_net")
317+
var diags diag.Diagnostics
318+
createReq.InitEndpoints, diags = expandPrivateNetwork(pn, pnExist, ipamConfig, staticConfig)
319+
if diags.HasError() {
320+
return diags
318321
}
319-
createReq.InitEndpoints, err = expandPrivateNetwork(pn, pnExist, enableIpam)
320-
if err != nil {
321-
return diag.FromErr(err)
322+
for _, warning := range diags {
323+
tflog.Warn(ctx, warning.Detail)
322324
}
323325
}
324326
if _, lbExists := d.GetOk("load_balancer"); lbExists {
@@ -467,7 +469,7 @@ func resourceScalewayRdbInstanceRead(ctx context.Context, d *schema.ResourceData
467469
_ = d.Set("init_settings", flattenInstanceSettings(res.InitSettings))
468470

469471
// set endpoints
470-
enableIpam, err := isIpamEndpoint(res, meta)
472+
enableIpam, err := getIPAMConfigRead(res, meta)
471473
if err != nil {
472474
return diag.FromErr(err)
473475
}
@@ -710,19 +712,13 @@ func resourceScalewayRdbInstanceUpdate(ctx context.Context, d *schema.ResourceDa
710712
// set new endpoint
711713
pn, pnExist := d.GetOk("private_network")
712714
if pnExist {
713-
// "enable_ipam" is not readable from the API, so we just read the user's config
714-
enableIpam := true
715-
if rawConfig := d.GetRawConfig(); !rawConfig.IsNull() {
716-
pnRawConfig := rawConfig.AsValueMap()["private_network"].AsValueSlice()[0].AsValueMap()
717-
if !pnRawConfig["enable_ipam"].IsNull() && pnRawConfig["enable_ipam"].False() ||
718-
!pnRawConfig["ip_net"].IsNull() {
719-
enableIpam = false
720-
}
715+
ipamConfig, staticConfig := getIPConfigUpdate(d, "ip_net")
716+
privateEndpoints, diags := expandPrivateNetwork(pn, pnExist, ipamConfig, staticConfig)
717+
if diags.HasError() {
718+
return diags
721719
}
722-
723-
privateEndpoints, err := expandPrivateNetwork(pn, pnExist, enableIpam)
724-
if err != nil {
725-
return diag.FromErr(err)
720+
for _, warning := range diags {
721+
tflog.Warn(ctx, warning.Detail)
726722
}
727723
for _, e := range privateEndpoints {
728724
_, err := rdbAPI.CreateEndpoint(

0 commit comments

Comments
 (0)