-
Notifications
You must be signed in to change notification settings - Fork 157
feat(redis): add cluster connect command #5211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
384c8b6
68afb9d
1469a91
5a8a22f
a32d236
56989b9
b385d39
92b761f
b653726
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| 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 |
|---|---|---|
|
|
@@ -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" | ||
| ) | ||
|
|
@@ -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, | ||
|
||
| 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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! The |
||
| } | ||
|
|
||
| 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}`, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Rawinstead ofArgsJSONso they explicitly show theprivate-network=trueflag.