Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions internal/services/rdb/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,14 @@ func ResourceInstance() *schema.Resource {
Computed: true,
Description: "The endpoint ID",
},
"ip_net": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.IsCIDR,
Description: "The IP with the given mask within the private subnet",
},
"ip_net": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateDiagFunc: verify.IsStandaloneIPorCIDR(),
DiffSuppressFunc: dsf.DiffSuppressFuncStandaloneIPandCIDR,
Description: "The IP with the given mask within the private subnet",
},
"ip": {
Type: schema.TypeString,
Computed: true,
Expand Down
151 changes: 151 additions & 0 deletions internal/services/rdb/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,157 @@
})
}

func TestAccInstance_PrivateNetworkWithStandaloneIP(t *testing.T) {
tt := acctest.NewTestTools(t)
defer tt.Cleanup()

latestEngineVersion := rdbchecks.GetLatestEngineVersion(tt, postgreSQLEngineName)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },

Check failure on line 1691 in internal/services/rdb/instance_test.go

View workflow job for this annotation

GitHub Actions / tfproviderlint

undefined: acctest.PreCheck

Check failure on line 1691 in internal/services/rdb/instance_test.go

View workflow job for this annotation

GitHub Actions / tfproviderlint

undefined: acctest.PreCheck

Check failure on line 1691 in internal/services/rdb/instance_test.go

View workflow job for this annotation

GitHub Actions / terraform (rdb)

undefined: acctest.PreCheck

Check failure on line 1691 in internal/services/rdb/instance_test.go

View workflow job for this annotation

GitHub Actions / opentofu (rdb)

undefined: acctest.PreCheck
ProtoV6ProviderFactories: tt.ProviderFactories,
CheckDestroy: resource.ComposeTestCheckFunc(
rdbchecks.IsInstanceDestroyed(tt),
vpcchecks.CheckPrivateNetworkDestroy(tt),
),
Steps: []resource.TestStep{
// Test with standalone IP address (without CIDR notation)
{
Config: fmt.Sprintf(`
resource "scaleway_vpc" "main" {
name = "test-rdb-standalone-ip"
}

resource "scaleway_vpc_private_network" "pn" {
name = "test-pn-standalone-ip"
vpc_id = scaleway_vpc.main.id
ipv4_subnet {
subnet = "10.213.254.0/24"
}
}

resource "scaleway_rdb_instance" "main" {
name = "test-rdb-standalone-ip"
node_type = "db-dev-s"
engine = %q
is_ha_cluster = false
disable_backup = true
user_name = "test_user"
password = "thiZ_is_v&ry_s3cret"
tags = ["terraform-test", "rdb-standalone-ip"]
volume_type = "sbs_5k"
volume_size_in_gb = 10

private_network {
ip_net = "10.213.254.4/28"
pn_id = scaleway_vpc_private_network.pn.id
port = 5432
}
}
`, latestEngineVersion),
Check: resource.ComposeTestCheckFunc(
vpcchecks.IsPrivateNetworkPresent(tt, "scaleway_vpc_private_network.pn"),
isInstancePresent(tt, "scaleway_rdb_instance.main"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.#", "1"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.enable_ipam", "false"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.port", "5432"),
resource.TestCheckResourceAttrPair("scaleway_rdb_instance.main", "private_network.0.pn_id", "scaleway_vpc_private_network.pn", "id"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.ip_net", "10.213.254.4/28"),
resource.TestCheckResourceAttrSet("scaleway_rdb_instance.main", "private_network.0.ip"),
),
},
// Test with explicit CIDR notation /28
{
Config: fmt.Sprintf(`
resource "scaleway_vpc" "main" {
name = "test-rdb-standalone-ip"
}

resource "scaleway_vpc_private_network" "pn" {
name = "test-pn-standalone-ip"
vpc_id = scaleway_vpc.main.id
ipv4_subnet {
subnet = "10.213.254.0/24"
}
}

resource "scaleway_rdb_instance" "main" {
name = "test-rdb-standalone-ip"
node_type = "db-dev-s"
engine = %q
is_ha_cluster = false
disable_backup = true
user_name = "test_user"
password = "thiZ_is_v&ry_s3cret"
tags = ["terraform-test", "rdb-standalone-ip"]
volume_type = "sbs_5k"
volume_size_in_gb = 10

private_network {
ip_net = "10.213.254.20/28"
pn_id = scaleway_vpc_private_network.pn.id
port = 5432
}
}
`, latestEngineVersion),
Check: resource.ComposeTestCheckFunc(
vpcchecks.IsPrivateNetworkPresent(tt, "scaleway_vpc_private_network.pn"),
isInstancePresent(tt, "scaleway_rdb_instance.main"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.#", "1"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.enable_ipam", "false"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.port", "5432"),
resource.TestCheckResourceAttrPair("scaleway_rdb_instance.main", "private_network.0.pn_id", "scaleway_vpc_private_network.pn", "id"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.ip_net", "10.213.254.20/28"),
resource.TestCheckResourceAttrSet("scaleway_rdb_instance.main", "private_network.0.ip"),
),
},
// Test update with a different CIDR
{
Config: fmt.Sprintf(`
resource "scaleway_vpc" "main" {
name = "test-rdb-standalone-ip"
}

resource "scaleway_vpc_private_network" "pn" {
name = "test-pn-standalone-ip"
vpc_id = scaleway_vpc.main.id
ipv4_subnet {
subnet = "10.213.254.0/24"
}
}

resource "scaleway_rdb_instance" "main" {
name = "test-rdb-standalone-ip"
node_type = "db-dev-s"
engine = %q
is_ha_cluster = false
disable_backup = true
user_name = "test_user"
password = "thiZ_is_v&ry_s3cret"
tags = ["terraform-test", "rdb-standalone-ip"]
volume_type = "sbs_5k"
volume_size_in_gb = 10

private_network {
ip_net = "10.213.254.36/28"
pn_id = scaleway_vpc_private_network.pn.id
port = 5432
}
}
`, latestEngineVersion),
Check: resource.ComposeTestCheckFunc(
vpcchecks.IsPrivateNetworkPresent(tt, "scaleway_vpc_private_network.pn"),
isInstancePresent(tt, "scaleway_rdb_instance.main"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.#", "1"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.enable_ipam", "false"),
resource.TestCheckResourceAttr("scaleway_rdb_instance.main", "private_network.0.ip_net", "10.213.254.36/28"),
resource.TestCheckResourceAttrSet("scaleway_rdb_instance.main", "private_network.0.ip"),
),
},
},
})
}

func isInstancePresent(tt *acctest.TestTools, n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down
16 changes: 8 additions & 8 deletions internal/services/rdb/read_replica.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/cdf"
Expand Down Expand Up @@ -106,13 +105,14 @@ func ResourceReadReplica() *schema.Resource {
DiffSuppressFunc: dsf.Locality,
Required: true,
},
"service_ip": {
Type: schema.TypeString,
Description: "The IP network address within the private subnet",
Optional: true,
Computed: true,
ValidateFunc: validation.IsCIDR,
},
"service_ip": {
Type: schema.TypeString,
Description: "The IP network address within the private subnet",
Optional: true,
Computed: true,
ValidateDiagFunc: verify.IsStandaloneIPorCIDR(),
DiffSuppressFunc: dsf.DiffSuppressFuncStandaloneIPandCIDR,
},
"enable_ipam": {
Type: schema.TypeBool,
Optional: true,
Expand Down

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions internal/services/rdb/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ func expandPrivateNetwork(data any, exist bool, ipamConfig *bool, staticConfig *
}

if staticConfig != nil {
ip, err := types.ExpandIPNet(*staticConfig)
// Normalize IP to CIDR notation if needed (e.g., 10.0.0.1 -> 10.0.0.1/32)
normalizedIP, err := types.NormalizeIPToCIDR(*staticConfig)
if err != nil {
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network ip_net (%s): %w", r["ip_net"], err))...)
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to normalize private_network ip_net (%s): %w", r["ip_net"], err))...)
}

ip, err := types.ExpandIPNet(normalizedIP)
if err != nil {
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network ip_net (%s): %w", normalizedIP, err))...)
}

spec.PrivateNetwork.ServiceIP = &ip
Expand Down Expand Up @@ -175,9 +181,15 @@ func expandReadReplicaEndpointsSpecPrivateNetwork(data any, ipamConfig *bool, st
}

if staticConfig != nil {
ipNet, err := types.ExpandIPNet(*staticConfig)
// Normalize IP to CIDR notation if needed (e.g., 10.0.0.1 -> 10.0.0.1/32)
normalizedIP, err := types.NormalizeIPToCIDR(*staticConfig)
if err != nil {
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to normalize private_network service_ip (%s): %w", rawEndpoint["service_ip"], err))...)
}

ipNet, err := types.ExpandIPNet(normalizedIP)
if err != nil {
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network service_ip (%s): %w", rawEndpoint["service_ip"], err))...)
return nil, append(diags, diag.FromErr(fmt.Errorf("failed to parse private_network service_ip (%s): %w", normalizedIP, err))...)
}

endpoint.PrivateNetwork.ServiceIP = &ipNet
Expand Down
29 changes: 29 additions & 0 deletions internal/types/ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,32 @@ func FlattenIPNet(ipNet scw.IPNet) (string, error) {

return string(raw[1 : len(raw)-1]), nil // remove quotes
}

// NormalizeIPToCIDR converts a standalone IP address to CIDR notation with default mask
// If the input is already in CIDR notation, it returns it unchanged
// IPv4 addresses get /32 mask, IPv6 addresses get /128 mask
func NormalizeIPToCIDR(raw string) (string, error) {
if raw == "" {
return "", nil
}

// Check if it's already a valid CIDR
if _, _, err := net.ParseCIDR(raw); err == nil {
return raw, nil
}

// Try to parse as standalone IP
ip := net.ParseIP(raw)
if ip == nil {
return "", fmt.Errorf("invalid IP address or CIDR notation: %s", raw)
}

// Add default mask based on IP version
if ip.To4() != nil {
// IPv4 address
return raw + "/32", nil
}

// IPv6 address
return raw + "/128", nil
}
105 changes: 105 additions & 0 deletions internal/types/ip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package types_test

import (
"testing"

"github.com/scaleway/terraform-provider-scaleway/v2/internal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNormalizeIPToCIDR(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectError bool
}{
{
name: "IPv4 address without mask",
input: "10.213.254.3",
expected: "10.213.254.3/32",
expectError: false,
},
{
name: "IPv4 address with /32 mask",
input: "10.213.254.3/32",
expected: "10.213.254.3/32",
expectError: false,
},
{
name: "IPv4 address with /24 mask",
input: "192.168.1.0/24",
expected: "192.168.1.0/24",
expectError: false,
},
{
name: "IPv4 address with /16 mask",
input: "10.0.0.0/16",
expected: "10.0.0.0/16",
expectError: false,
},
{
name: "IPv6 address without mask",
input: "2001:db8::1",
expected: "2001:db8::1/128",
expectError: false,
},
{
name: "IPv6 address with /128 mask",
input: "2001:db8::1/128",
expected: "2001:db8::1/128",
expectError: false,
},
{
name: "IPv6 address with /64 mask",
input: "2001:db8::/64",
expected: "2001:db8::/64",
expectError: false,
},
{
name: "empty string",
input: "",
expected: "",
expectError: false,
},
{
name: "invalid IP",
input: "not-an-ip",
expected: "",
expectError: true,
},
{
name: "invalid CIDR",
input: "10.0.0.1/33",
expected: "",
expectError: true,
},
{
name: "localhost IPv4",
input: "127.0.0.1",
expected: "127.0.0.1/32",
expectError: false,
},
{
name: "localhost IPv6",
input: "::1",
expected: "::1/128",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := types.NormalizeIPToCIDR(tt.input)

if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}

Loading