Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
🎲🎲🎲 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

Connect to a Redis cluster via private network
scw redis cluster connect
Copy link
Member

Choose a reason for hiding this comment

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

Is it normal that the private network is not mentioned here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! I've updated the examples to use Raw instead of ArgsJSON so they explicitly show the private-network=true flag.


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
[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
38 changes: 38 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,43 @@ 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 |
| 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
```

Connect to a Redis cluster via private network
```
scw redis cluster connect
```




### 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
212 changes: 212 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,208 @@ func autoCompleteNodeType(

return suggestions
}

type clusterConnectArgs struct {
Zone scw.Zone
PrivateNetwork bool
ClusterID string
CliRedis *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 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",
},
core.ZoneArgSpec(
scw.ZoneFrPar1,
Copy link
Member

Choose a reason for hiding this comment

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

Could it be defined with a zones coming from the SDK instead of explicitly?

scw.ZoneFrPar2,
scw.ZoneNlAms1,
scw.ZoneNlAms2,
scw.ZonePlWaw1,
scw.ZonePlWaw2,
),
},
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)
}

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",
ArgsJSON: `{"cluster-id": "11111111-1111-1111-1111-111111111111"}`,
},
{
Short: "Connect to a Redis cluster via private network",
ArgsJSON: `{"cluster-id": "11111111-1111-1111-1111-111111111111", "private-network": true}`,
},
},
}
}
Loading