Skip to content

Commit 6c6fc95

Browse files
authored
feat(rdb): ipam config for private endpoints (#3521)
1 parent ea6fdae commit 6c6fc95

13 files changed

+2158
-948
lines changed

cmd/scw/testdata/test-all-usage-rdb-instance-create-usage.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ARGS:
2020
[init-settings.{index}.value]
2121
[volume-type] Type of volume where data is stored (lssd, bssd, ...) (lssd | bssd | sbs_5k | sbs_15k)
2222
[volume-size] Volume size when volume_type is not lssd
23+
[init-endpoints.{index}.private-network.enable-ipam=false] Will configure your Private Network endpoint with Scaleway IPAM service if true
2324
[init-endpoints.{index}.private-network.private-network-id] UUID of the Private Network to be connected to the Database Instance
2425
[init-endpoints.{index}.private-network.service-ip] Endpoint IPv4 address with a CIDR notation. Refer to the official Scaleway documentation to learn more about IP and subnet limitations.
2526
[backup-same-region] Defines whether to or not to store logical backups in the same region as the Database Instance

cmd/scw/testdata/test-all-usage-rdb-instance-delete-usage.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ARGS:
1111

1212
FLAGS:
1313
-h, --help help for delete
14+
-w, --wait wait until the instance is ready
1415

1516
GLOBAL FLAGS:
1617
-c, --config string The path to the config file

docs/commands/rdb.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ scw rdb instance create [arg=value ...]
692692
| init-settings.{index}.value | | |
693693
| volume-type | One of: `lssd`, `bssd`, `sbs_5k`, `sbs_15k` | Type of volume where data is stored (lssd, bssd, ...) |
694694
| volume-size | | Volume size when volume_type is not lssd |
695+
| init-endpoints.{index}.private-network.enable-ipam | Default: `false` | Will configure your Private Network endpoint with Scaleway IPAM service if true |
695696
| init-endpoints.{index}.private-network.private-network-id | | UUID of the Private Network to be connected to the Database Instance |
696697
| init-endpoints.{index}.private-network.service-ip | | Endpoint IPv4 address with a CIDR notation. Refer to the official Scaleway documentation to learn more about IP and subnet limitations. |
697698
| backup-same-region | | Defines whether to or not to store logical backups in the same region as the Database Instance |

internal/namespaces/rdb/v1/custom.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func GetCommands() *core.Commands {
3939
cmds.MustFind("rdb", "instance", "upgrade").Override(instanceUpgradeBuilder)
4040
cmds.MustFind("rdb", "instance", "update").Override(instanceUpdateBuilder)
4141
cmds.MustFind("rdb", "instance", "get").Override(instanceGetBuilder)
42+
cmds.MustFind("rdb", "instance", "delete").Override(instanceDeleteBuilder)
4243

4344
cmds.MustFind("rdb", "engine", "list").Override(engineListBuilder)
4445

internal/namespaces/rdb/v1/custom_instance.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package rdb
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"net/http"
68
"os"
79
"os/exec"
810
"path"
@@ -190,11 +192,27 @@ func autoCompleteNodeType(ctx context.Context, prefix string) core.AutocompleteS
190192
}
191193

192194
func instanceCreateBuilder(c *core.Command) *core.Command {
195+
type rdbEndpointSpecPrivateNetworkCustom struct {
196+
*rdb.EndpointSpecPrivateNetwork
197+
EnableIpam bool `json:"enable-ipam"`
198+
}
199+
200+
type rdbEndpointSpecCustom struct {
201+
PrivateNetwork *rdbEndpointSpecPrivateNetworkCustom `json:"private-network"`
202+
}
203+
193204
type rdbCreateInstanceRequestCustom struct {
194205
*rdb.CreateInstanceRequest
206+
InitEndpoints []*rdbEndpointSpecCustom `json:"init-endpoints"`
195207
GeneratePassword bool
196208
}
197209

210+
c.ArgSpecs.AddBefore("init-endpoints.{index}.private-network.private-network-id", &core.ArgSpec{
211+
Name: "init-endpoints.{index}.private-network.enable-ipam",
212+
Short: "Will configure your Private Network endpoint with Scaleway IPAM service if true",
213+
Required: false,
214+
Default: core.DefaultValueSetter("false"),
215+
})
198216
c.ArgSpecs.AddBefore("password", &core.ArgSpec{
199217
Name: "generate-password",
200218
Short: `Will generate a 21 character-length password that contains a mix of upper/lower case letters, numbers and special symbols`,
@@ -247,6 +265,23 @@ func instanceCreateBuilder(c *core.Command) *core.Command {
247265
fmt.Printf("\n")
248266
}
249267

268+
for _, customEndpoint := range customRequest.InitEndpoints {
269+
if customEndpoint.PrivateNetwork == nil {
270+
continue
271+
}
272+
ipamConfig := &rdb.EndpointSpecPrivateNetworkIpamConfig{}
273+
if !customEndpoint.PrivateNetwork.EnableIpam {
274+
ipamConfig = nil
275+
}
276+
createInstanceRequest.InitEndpoints = append(createInstanceRequest.InitEndpoints, &rdb.EndpointSpec{
277+
PrivateNetwork: &rdb.EndpointSpecPrivateNetwork{
278+
PrivateNetworkID: customEndpoint.PrivateNetwork.PrivateNetworkID,
279+
ServiceIP: customEndpoint.PrivateNetwork.ServiceIP,
280+
IpamConfig: ipamConfig,
281+
},
282+
})
283+
}
284+
250285
instance, err := api.CreateInstance(createInstanceRequest)
251286
if err != nil {
252287
return nil, err
@@ -512,6 +547,29 @@ func instanceUpdateBuilder(_ *core.Command) *core.Command {
512547
}
513548
}
514549

550+
func instanceDeleteBuilder(c *core.Command) *core.Command {
551+
c.WaitFunc = func(ctx context.Context, argsI, respI interface{}) (interface{}, error) {
552+
api := rdb.NewAPI(core.ExtractClient(ctx))
553+
instance, err := api.WaitForInstance(&rdb.WaitForInstanceRequest{
554+
InstanceID: respI.(*rdb.Instance).ID,
555+
Region: respI.(*rdb.Instance).Region,
556+
Timeout: scw.TimeDurationPtr(instanceActionTimeout),
557+
RetryInterval: core.DefaultRetryInterval,
558+
})
559+
if err != nil {
560+
// if we get a 404 here, it means the resource was successfully deleted
561+
notFoundError := &scw.ResourceNotFoundError{}
562+
responseError := &scw.ResponseError{}
563+
if errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound || errors.As(err, &notFoundError) {
564+
return instance, nil
565+
}
566+
return nil, err
567+
}
568+
return instance, nil
569+
}
570+
return c
571+
}
572+
515573
func instanceWaitCommand() *core.Command {
516574
return &core.Command{
517575
Short: `Wait for an instance to reach a stable state`,

internal/namespaces/rdb/v1/custom_instance_test.go

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import (
66

77
"github.com/alecthomas/assert"
88
"github.com/scaleway/scaleway-cli/v2/internal/core"
9+
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/vpc/v2"
10+
"github.com/scaleway/scaleway-sdk-go/api/ipam/v1"
911
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
12+
"github.com/scaleway/scaleway-sdk-go/scw"
13+
)
14+
15+
const (
16+
publicEndpoint = "public"
17+
privateEndpointIpam = "private IPAM"
18+
privateEndpointStatic = "private static"
1019
)
1120

1221
func Test_ListInstance(t *testing.T) {
@@ -31,19 +40,72 @@ func Test_CloneInstance(t *testing.T) {
3140

3241
func Test_CreateInstance(t *testing.T) {
3342
t.Run("Simple", core.Test(&core.TestConfig{
34-
Commands: GetCommands(),
35-
Cmd: fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s user-name=%s password=%s --wait", name, engine, user, password),
36-
Check: core.TestCheckGolden(),
43+
Commands: GetCommands(),
44+
Cmd: fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s user-name=%s password=%s --wait", name, engine, user, password),
45+
Check: core.TestCheckCombine(
46+
core.TestCheckGolden(),
47+
func(t *testing.T, ctx *core.CheckFuncCtx) {
48+
checkEndpoints(ctx, t, []string{publicEndpoint})
49+
},
50+
),
3751
AfterFunc: core.ExecAfterCmd("scw rdb instance delete {{ .CmdResult.ID }}"),
3852
}))
3953

4054
t.Run("With password generator", core.Test(&core.TestConfig{
4155
Commands: GetCommands(),
4256
Cmd: fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s user-name=%s generate-password=true --wait", name, engine, user),
4357
// do not check the golden as the password generated locally and on CI will necessarily be different
44-
Check: core.TestCheckExitCode(0),
58+
Check: core.TestCheckCombine(
59+
core.TestCheckExitCode(0),
60+
func(t *testing.T, ctx *core.CheckFuncCtx) {
61+
checkEndpoints(ctx, t, []string{publicEndpoint})
62+
},
63+
),
4564
AfterFunc: core.ExecAfterCmd("scw rdb instance delete {{ .CmdResult.ID }}"),
4665
}))
66+
67+
cmds := GetCommands()
68+
cmds.Merge(vpc.GetCommands())
69+
70+
t.Run("With static private endpoint", core.Test(&core.TestConfig{
71+
Commands: cmds,
72+
BeforeFunc: core.BeforeFuncCombine(
73+
core.ExecStoreBeforeCmd("PrivateNetwork", "scw vpc private-network create"),
74+
),
75+
Cmd: fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s "+
76+
"user-name=%s password=%s init-endpoints.0.private-network.private-network-id={{ .PrivateNetwork.ID }} "+
77+
"init-endpoints.0.private-network.service-ip=172.16.4.1/22 --wait", name, engine, user, password),
78+
Check: core.TestCheckCombine(
79+
core.TestCheckGolden(),
80+
func(t *testing.T, ctx *core.CheckFuncCtx) {
81+
checkEndpoints(ctx, t, []string{publicEndpoint, privateEndpointStatic})
82+
},
83+
),
84+
AfterFunc: core.AfterFuncCombine(
85+
core.ExecAfterCmd("scw rdb instance delete {{ .CmdResult.ID }} --wait"),
86+
deletePrivateNetwork("PrivateNetwork"),
87+
),
88+
}))
89+
90+
t.Run("With IPAM private endpoint", core.Test(&core.TestConfig{
91+
Commands: cmds,
92+
BeforeFunc: core.BeforeFuncCombine(
93+
core.ExecStoreBeforeCmd("PrivateNetwork", "scw vpc private-network create"),
94+
),
95+
Cmd: fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s "+
96+
"user-name=%s password=%s init-endpoints.0.private-network.private-network-id={{ .PrivateNetwork.ID }} "+
97+
"init-endpoints.0.private-network.enable-ipam=true --wait", name, engine, user, password),
98+
Check: core.TestCheckCombine(
99+
core.TestCheckGolden(),
100+
func(t *testing.T, ctx *core.CheckFuncCtx) {
101+
checkEndpoints(ctx, t, []string{publicEndpoint, privateEndpointIpam})
102+
},
103+
),
104+
AfterFunc: core.AfterFuncCombine(
105+
core.ExecAfterCmd("scw rdb instance delete {{ .CmdResult.ID }} --wait"),
106+
deletePrivateNetwork("PrivateNetwork"),
107+
),
108+
}))
47109
}
48110

49111
func Test_GetInstance(t *testing.T) {
@@ -204,3 +266,53 @@ func Test_Connect(t *testing.T) {
204266
AfterFunc: deleteInstance(),
205267
}))
206268
}
269+
270+
func deletePrivateNetwork(metaName string) core.AfterFunc {
271+
return core.ExecAfterCmd(fmt.Sprintf("scw vpc private-network delete {{ .%s.ID }}", metaName))
272+
}
273+
274+
func checkEndpoints(ctx *core.CheckFuncCtx, t *testing.T, expected []string) {
275+
instance := ctx.Result.(createInstanceResult).Instance
276+
ipamAPI := ipam.NewAPI(ctx.Client)
277+
var foundEndpoints = map[string]bool{}
278+
279+
for _, endpoint := range instance.Endpoints {
280+
if endpoint.LoadBalancer != nil {
281+
foundEndpoints[publicEndpoint] = true
282+
}
283+
if endpoint.PrivateNetwork != nil {
284+
ips, err := ipamAPI.ListIPs(&ipam.ListIPsRequest{
285+
Region: instance.Region,
286+
ResourceID: &instance.ID,
287+
ResourceType: "rdb_instance",
288+
IsIPv6: scw.BoolPtr(false),
289+
}, scw.WithAllPages())
290+
if err != nil {
291+
t.Errorf("could not list IPs: %v", err)
292+
}
293+
switch ips.TotalCount {
294+
case 1:
295+
foundEndpoints[privateEndpointIpam] = true
296+
case 0:
297+
foundEndpoints[privateEndpointStatic] = true
298+
default:
299+
t.Errorf("expected no more than 1 IP for instance, got %d", ips.TotalCount)
300+
}
301+
}
302+
}
303+
304+
// Check that every expected endpoint got found
305+
for _, e := range expected {
306+
_, ok := foundEndpoints[e]
307+
if !ok {
308+
t.Errorf("expected a %s endpoint but got none", e)
309+
}
310+
delete(foundEndpoints, e)
311+
}
312+
// Check that no unexpected endpoint was found
313+
if len(foundEndpoints) > 0 {
314+
for e := range foundEndpoints {
315+
t.Errorf("found a %s endpoint when none was expected", e)
316+
}
317+
}
318+
}

0 commit comments

Comments
 (0)