Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
Connect to a Redis cluster using locally installed redis-cli. The command will check if redis-cli is installed, download the certificate if TLS is enabled, and prompt for the password.

USAGE:
scw redis cluster connect <cluster-id ...> [arg=value ...]

EXAMPLES:
Connect to a Redis cluster
scw redis cluster connect 11111111-1111-1111-1111-111111111111

Connect to a Redis cluster via private network
scw redis cluster connect 11111111-1111-1111-1111-111111111111 private-network=true

ARGS:
[private-network=false] Connect by the private network endpoint attached.
cluster-id UUID of the cluster
[cli-redis] Command line tool to use, default to redis-cli
[cli-args] Additional arguments to pass to redis-cli
[zone=fr-par-1] Zone to target. If none is passed will use default zone from the config (fr-par-1 | fr-par-2 | nl-ams-1 | nl-ams-2 | pl-waw-1 | pl-waw-2)

FLAGS:
-h, --help help for connect

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ USAGE:
scw redis cluster <command>

AVAILABLE COMMANDS:
connect Connect to a Redis cluster using locally installed redis-cli
create Create a Redis™ Database Instance
delete Delete a Redis™ Database Instance
get Get a Redis™ Database Instance
Expand Down
39 changes: 39 additions & 0 deletions docs/commands/redis.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This API allows you to manage your Managed Databases for Redis™.
- [Set ACL rules for a cluster](#set-acl-rules-for-a-cluster)
- [Update an ACL rule for a Redis™ Database Instance (network rule)](#update-an-acl-rule-for-a-redis™-database-instance-(network-rule))
- [Cluster management commands](#cluster-management-commands)
- [Connect to a Redis cluster using locally installed redis-cli](#connect-to-a-redis-cluster-using-locally-installed-redis-cli)
- [Create a Redis™ Database Instance](#create-a-redis™-database-instance)
- [Delete a Redis™ Database Instance](#delete-a-redis™-database-instance)
- [Get a Redis™ Database Instance](#get-a-redis™-database-instance)
Expand Down Expand Up @@ -153,6 +154,44 @@ scw redis acl update <acl-id ...> [arg=value ...]
A Redis™ Database Instance, also known as a Redis™ cluster, consists of either one standalone node or a cluster composed of three to six nodes. The cluster uses partitioning to split the keyspace. Each partition is replicated and can be reassigned or elected as the primary when necessary. Standalone mode creates a standalone database provisioned on a single node.


### Connect to a Redis cluster using locally installed redis-cli

Connect to a Redis cluster using locally installed redis-cli. The command will check if redis-cli is installed, download the certificate if TLS is enabled, and prompt for the password.

**Usage:**

```
scw redis cluster connect <cluster-id ...> [arg=value ...]
```


**Args:**

| Name | | Description |
|------|---|-------------|
| private-network | Default: `false` | Connect by the private network endpoint attached. |
| cluster-id | Required | UUID of the cluster |
| cli-redis | | Command line tool to use, default to redis-cli |
| cli-args | | Additional arguments to pass to redis-cli |
| zone | Default: `fr-par-1`<br />One of: `fr-par-1`, `fr-par-2`, `nl-ams-1`, `nl-ams-2`, `pl-waw-1`, `pl-waw-2` | Zone to target. If none is passed will use default zone from the config |


**Examples:**


Connect to a Redis cluster
```
scw redis cluster connect 11111111-1111-1111-1111-111111111111
```

Connect to a Redis cluster via private network
```
scw redis cluster connect 11111111-1111-1111-1111-111111111111 private-network=true
```




### Create a Redis™ Database Instance

Create a new Redis™ Database Instance (Redis™ cluster). You must set the `zone`, `project_id`, `version`, `node_type`, `user_name` and `password` parameters. Optionally you can define `acl_rules`, `endpoints`, `tls_enabled` and `cluster_settings`.
Expand Down
2 changes: 1 addition & 1 deletion internal/namespaces/redis/v1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func GetCommands() *core.Commands {

human.RegisterMarshalerFunc(redis.Cluster{}, redisClusterGetMarshalerFunc)

cmds.Merge(core.NewCommands(clusterWaitCommand()))
cmds.Merge(core.NewCommands(clusterWaitCommand(), clusterConnectCommand()))
cmds.MustFind("redis", "cluster", "create").Override(clusterCreateBuilder)
cmds.MustFind("redis", "cluster", "delete").Override(clusterDeleteBuilder)
cmds.MustFind("redis", "acl", "add").Override(ACLAddListBuilder)
Expand Down
226 changes: 226 additions & 0 deletions internal/namespaces/redis/v1/custom_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ package redis
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"

"github.com/scaleway/scaleway-cli/v2/core"
"github.com/scaleway/scaleway-cli/v2/core/human"
"github.com/scaleway/scaleway-cli/v2/internal/interactive"
"github.com/scaleway/scaleway-sdk-go/api/redis/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
Expand Down Expand Up @@ -316,3 +323,222 @@ func autoCompleteNodeType(

return suggestions
}

type clusterConnectArgs struct {
Zone scw.Zone
PrivateNetwork bool
ClusterID string
CliRedis *string
CliArgs []string
}

const (
errorMessagePublicEndpointNotFound = "public endpoint not found"
errorMessagePrivateEndpointNotFound = "private endpoint not found"
errorMessageEndpointNotFound = "any endpoint is associated on your cluster"
errorMessageRedisCliNotFound = "redis-cli is not installed. Please install redis-cli to use this command"
)

func getPublicEndpoint(endpoints []*redis.Endpoint) (*redis.Endpoint, error) {
for _, e := range endpoints {
if e.PublicNetwork != nil {
return e, nil
}
}

return nil, fmt.Errorf("%s", errorMessagePublicEndpointNotFound)
}

func getPrivateEndpoint(endpoints []*redis.Endpoint) (*redis.Endpoint, error) {
for _, e := range endpoints {
if e.PrivateNetwork != nil {
return e, nil
}
}

return nil, fmt.Errorf("%s", errorMessagePrivateEndpointNotFound)
}

func checkRedisCliInstalled(cliRedis string) error {
cmd := exec.Command(cliRedis, "--version") //nolint:gosec
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s", errorMessageRedisCliNotFound)
}

return nil
}

func getRedisZones() []scw.Zone {
// Get zones dynamically from the Redis API SDK
// We create a minimal client just to access the Zones() method
// which doesn't require authentication
client := &scw.Client{}
api := redis.NewAPI(client)

return api.Zones()
}

func clusterConnectCommand() *core.Command {
return &core.Command{
Namespace: "redis",
Resource: "cluster",
Verb: "connect",
Short: "Connect to a Redis cluster using locally installed redis-cli",
Long: "Connect to a Redis cluster using locally installed redis-cli. The command will check if redis-cli is installed, download the certificate if TLS is enabled, and prompt for the password.",
ArgsType: reflect.TypeOf(clusterConnectArgs{}),
ArgSpecs: core.ArgSpecs{
{
Name: "private-network",
Short: `Connect by the private network endpoint attached.`,
Required: false,
Default: core.DefaultValueSetter("false"),
},
{
Name: "cluster-id",
Short: `UUID of the cluster`,
Required: true,
Positional: true,
},
{
Name: "cli-redis",
Short: "Command line tool to use, default to redis-cli",
},
{
Name: "cli-args",
Short: "Additional arguments to pass to redis-cli",
Required: false,
},
core.ZoneArgSpec(getRedisZones()...),
},
Run: func(ctx context.Context, argsI any) (any, error) {
args := argsI.(*clusterConnectArgs)

cliRedis := "redis-cli"
if args.CliRedis != nil {
cliRedis = *args.CliRedis
}

if err := checkRedisCliInstalled(cliRedis); err != nil {
return nil, err
}

client := core.ExtractClient(ctx)
api := redis.NewAPI(client)
cluster, err := api.GetCluster(&redis.GetClusterRequest{
Zone: args.Zone,
ClusterID: args.ClusterID,
})
if err != nil {
return nil, err
}

if len(cluster.Endpoints) == 0 {
return nil, fmt.Errorf("%s", errorMessageEndpointNotFound)
}

var endpoint *redis.Endpoint
switch {
case args.PrivateNetwork:
endpoint, err = getPrivateEndpoint(cluster.Endpoints)
if err != nil {
return nil, err
}
default:
endpoint, err = getPublicEndpoint(cluster.Endpoints)
if err != nil {
return nil, err
}
}

if len(endpoint.IPs) == 0 {
return nil, errors.New("endpoint has no IP addresses")
}

port := endpoint.Port

var certPath string
if cluster.TLSEnabled {
certResp, err := api.GetClusterCertificate(&redis.GetClusterCertificateRequest{
Zone: args.Zone,
ClusterID: args.ClusterID,
})
if err != nil {
return nil, fmt.Errorf("failed to get certificate: %w", err)
}

certContent, err := io.ReadAll(certResp.Content)
if err != nil {
return nil, fmt.Errorf("failed to read certificate content: %w", err)
}

tmpDir := os.TempDir()
certPath = filepath.Join(tmpDir, fmt.Sprintf("redis-cert-%s.crt", args.ClusterID))
if err := os.WriteFile(certPath, certContent, 0o600); err != nil {
return nil, fmt.Errorf("failed to write certificate: %w", err)
}
defer func() {
if err := os.Remove(certPath); err != nil {
core.ExtractLogger(ctx).Debugf("failed to remove certificate file: %v", err)
}
}()
}

password, err := interactive.PromptPasswordWithConfig(&interactive.PromptPasswordConfig{
Ctx: ctx,
Prompt: "Password",
})
if err != nil {
return nil, fmt.Errorf("failed to get password: %w", err)
}

hostStr := endpoint.IPs[0].String()
cmdArgs := []string{
cliRedis,
"-h", hostStr,
"-p", strconv.FormatUint(uint64(port), 10),
"-a", password,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to have an argument passed through the command line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! The cli-args parameter allows passing additional arguments to redis-cli. Example: scw redis cluster connect <cluster-id> cli-args.0=--raw

}

if cluster.TLSEnabled {
cmdArgs = append(cmdArgs, "--tls", "--cert", certPath)
}

if cluster.UserName != "" {
cmdArgs = append(cmdArgs, "--user", cluster.UserName)
}

// Add any additional arguments passed by the user
if len(args.CliArgs) > 0 {
cmdArgs = append(cmdArgs, args.CliArgs...)
}

cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
core.ExtractLogger(ctx).Debugf("executing: %s\n", cmd.Args)

if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return nil, &core.CliError{Empty: true, Code: exitError.ExitCode()}
}

return nil, err
}

return &core.SuccessResult{
Empty: true,
}, nil
},
Examples: []*core.Example{
{
Short: "Connect to a Redis cluster",
Raw: `scw redis cluster connect 11111111-1111-1111-1111-111111111111`,
},
{
Short: "Connect to a Redis cluster via private network",
Raw: `scw redis cluster connect 11111111-1111-1111-1111-111111111111 private-network=true`,
},
},
}
}
Loading