Skip to content

Commit 4548028

Browse files
authored
ICU-14484: redis connect helper (#6001)
* add redis connect support * Setup redis in docker for e2e testing * Update docs and changelog * Commenting out e2e tests for now, will revisit in a separate task
1 parent f8a5894 commit 4548028

File tree

10 files changed

+386
-0
lines changed

10 files changed

+386
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
1616
This new helper command allows users to authorize sessions against Cassandra
1717
targets and automatically invoke a Cassandra client with the appropriate
1818
connection parameters and credentials. Currently only username/password credentials are automatically attached.
19+
* cli: Added `boundary connect redis` command for connecting to Redis targets.
20+
This new helper command allows users to authorize sessions against Redis
21+
targets and automatically invoke a Redis client with the appropriate
22+
connection parameters and credentials. Currently only username/password credentials are automatically attached.
1923
* ui: Improved load times for resource tables with search and filtering capabilities by replacing indexeddb for local data storage with sqlite (WASM) and OPFS ([PR](https://github.com/hashicorp/boundary-ui/pull/2984))
2024

2125
### Bug fixes
2226
* ui: Fixed rendering bug where header for the Host details page rendered multiple times ([PR](https://github.com/hashicorp/boundary-ui/pull/2980))
2327
* ui: Fixed bug where worker tags could not be removed when creating a new worker ([PR](https://github.com/hashicorp/boundary-ui/pull/2928))
2428

29+
2530
### Deprecations/Changes
2631

2732
* Modified parsing logic for various IP/host/address fields across Boundary.

enos/modules/test_e2e_docker/test.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ install_cassandra() {
4141
# Install Cassandra
4242
install_cassandra
4343

44+
# Install Redis
45+
apt install redis-server -y
4446

4547
# Create a GPG key
4648
export KEY_PW=boundary

internal/cmd/commands.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
423423
Func: "cassandra",
424424
}
425425
}),
426+
"connect redis": wrapper.Wrap(func() wrapper.WrappableCommand {
427+
return &connect.Command{
428+
Command: base.NewCommand(ui, opts...),
429+
Func: "redis",
430+
}
431+
}),
426432
"connect rdp": wrapper.Wrap(func() wrapper.WrappableCommand {
427433
return &connect.Command{
428434
Command: base.NewCommand(ui, opts...),

internal/cmd/commands/connect/connect.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ type Command struct {
8484
// Cassandra
8585
cassandraFlags
8686

87+
// Redis
88+
redisFlags
89+
8790
// RDP
8891
rdpFlags
8992

@@ -114,6 +117,8 @@ func (c *Command) Synopsis() string {
114117
return mysqlSynopsis
115118
case "cassandra":
116119
return cassandraSynopsis
120+
case "redis":
121+
return redisSynopsis
117122
case "rdp":
118123
return rdpSynopsis
119124
case "ssh":
@@ -239,6 +244,9 @@ func (c *Command) Flags() *base.FlagSets {
239244
case "cassandra":
240245
cassandraOptions(c, set)
241246

247+
case "redis":
248+
redisOptions(c, set)
249+
242250
case "rdp":
243251
rdpOptions(c, set)
244252

@@ -330,6 +338,8 @@ func (c *Command) Run(args []string) (retCode int) {
330338
c.flagExec = c.mysqlFlags.defaultExec()
331339
case "cassandra":
332340
c.flagExec = c.cassandraFlags.defaultExec()
341+
case "redis":
342+
c.flagExec = c.redisFlags.defaultExec()
333343
case "rdp":
334344
c.flagExec = c.rdpFlags.defaultExec()
335345
case "kube":
@@ -714,6 +724,16 @@ func (c *Command) handleExec(clientProxy *apiproxy.ClientProxy, passthroughArgs
714724
envs = append(envs, cassandraEnvs...)
715725
creds = cassandraCreds
716726

727+
case "redis":
728+
redisArgs, redisEnvs, redisCreds, redisErr := c.redisFlags.buildArgs(c, port, host, addr, creds)
729+
if redisErr != nil {
730+
argsErr = redisErr
731+
break
732+
}
733+
args = append(args, redisArgs...)
734+
envs = append(envs, redisEnvs...)
735+
creds = redisCreds
736+
717737
case "rdp":
718738
args = append(args, c.rdpFlags.buildArgs(c, port, host, addr)...)
719739

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package connect
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"github.com/hashicorp/boundary/api/proxy"
11+
"github.com/hashicorp/boundary/internal/cmd/base"
12+
"github.com/posener/complete"
13+
)
14+
15+
const (
16+
redisSynopsis = "Authorize a session against a target and invoke a redis client to connect"
17+
)
18+
19+
func redisOptions(c *Command, set *base.FlagSets) {
20+
f := set.NewFlagSet("Redis Options")
21+
22+
f.StringVar(&base.StringVar{
23+
Name: "style",
24+
Target: &c.flagRedisStyle,
25+
EnvVar: "BOUNDARY_CONNECT_REDIS_STYLE",
26+
Completion: complete.PredictSet("redis-cli"),
27+
Default: "redis-cli",
28+
Usage: `Specifies how the CLI will attempt to invoke a Redis client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "redis-cli".`,
29+
})
30+
31+
f.StringVar(&base.StringVar{
32+
Name: "username",
33+
Target: &c.flagUsername,
34+
EnvVar: "BOUNDARY_CONNECT_USERNAME",
35+
Completion: complete.PredictNothing,
36+
Usage: `Specifies the username to pass through to the client. May be overridden by credentials sourced from a credential store.`,
37+
})
38+
}
39+
40+
type redisFlags struct {
41+
flagRedisStyle string
42+
}
43+
44+
func (r *redisFlags) defaultExec() string {
45+
return strings.ToLower(r.flagRedisStyle)
46+
}
47+
48+
func (r *redisFlags) buildArgs(c *Command, port, ip, _ string, creds proxy.Credentials) (args, envs []string, retCreds proxy.Credentials, retErr error) {
49+
var username, password string
50+
51+
retCreds = creds
52+
if len(retCreds.UsernamePassword) > 0 {
53+
// Mark credential as consumed, such that it is not printed to the user
54+
retCreds.UsernamePassword[0].Consumed = true
55+
56+
// Grab the first available username/password credential brokered
57+
username = retCreds.UsernamePassword[0].Username
58+
password = retCreds.UsernamePassword[0].Password
59+
}
60+
61+
switch r.flagRedisStyle {
62+
case "redis-cli":
63+
args = append(args, "-h", ip)
64+
if port != "" {
65+
args = append(args, "-p", port)
66+
}
67+
68+
switch {
69+
case username != "":
70+
args = append(args, "--user", username)
71+
case c.flagUsername != "":
72+
args = append(args, "--user", c.flagUsername, "--askpass")
73+
}
74+
75+
// Password is read by redis-cli via environment variable. The password disappears after the command exits.
76+
if password != "" {
77+
envs = append(envs, fmt.Sprintf("REDISCLI_AUTH=%s", password))
78+
}
79+
}
80+
81+
return args, envs, retCreds, retErr
82+
}

testing/internal/e2e/infra/docker.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ type cassandraConfig struct {
3535
NetworkAlias string
3636
}
3737

38+
type redisConfig struct {
39+
User string
40+
Password string
41+
NetworkAlias string
42+
}
43+
3844
// StartBoundaryDatabase spins up a postgres database in a docker container.
3945
// Returns information about the container
4046
func StartBoundaryDatabase(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container {
@@ -421,6 +427,63 @@ func StartCassandra(t testing.TB, pool *dockertest.Pool, network *dockertest.Net
421427
}
422428
}
423429

430+
// StartRedis starts a Redis database in a docker container.
431+
// Returns information about the container
432+
func StartRedis(t testing.TB, pool *dockertest.Pool, network *dockertest.Network, repository, tag string) *Container {
433+
t.Log("Starting Redis database...")
434+
c, err := LoadConfig()
435+
require.NoError(t, err)
436+
437+
err = pool.Client.PullImage(docker.PullImageOptions{
438+
Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository),
439+
Tag: tag,
440+
}, docker.AuthConfiguration{})
441+
require.NoError(t, err)
442+
443+
config := redisConfig{
444+
User: "e2eboundary",
445+
Password: "e2eboundary",
446+
NetworkAlias: "e2eredis",
447+
}
448+
449+
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
450+
Repository: fmt.Sprintf("%s/%s", c.DockerMirror, repository),
451+
Tag: tag,
452+
ExposedPorts: []string{"6379/tcp"},
453+
Name: config.NetworkAlias,
454+
Networks: []*dockertest.Network{network},
455+
})
456+
require.NoError(t, err)
457+
458+
err = pool.Retry(func() error {
459+
cmd := exec.Command("docker", "exec", config.NetworkAlias, "redis-cli", "PING")
460+
output, cmdErr := cmd.CombinedOutput()
461+
if cmdErr != nil {
462+
return fmt.Errorf("failed to connect to Redis container '%s': %v\nOutput: %s", config.NetworkAlias, cmdErr, string(output))
463+
}
464+
return nil
465+
})
466+
require.NoError(t, err, "Redis container did not start in time or is not healthy")
467+
468+
err = setupRedisAuthAndUser(t, resource, pool, &config)
469+
require.NoError(t, err)
470+
471+
return &Container{
472+
Resource: resource,
473+
UriLocalhost: fmt.Sprintf(
474+
"redis://%s:%s@localhost:6379",
475+
config.User,
476+
config.Password,
477+
),
478+
UriNetwork: fmt.Sprintf(
479+
"redis://%s:%s@%s:6379",
480+
config.User,
481+
config.Password,
482+
config.NetworkAlias,
483+
),
484+
}
485+
}
486+
424487
// setupCassandraAuthAndUser enables authentication on a Cassandra container and creates a user with permissions.
425488
func setupCassandraAuthAndUser(t testing.TB, resource *dockertest.Resource, pool *dockertest.Pool, config *cassandraConfig) error {
426489
t.Helper()
@@ -479,3 +542,19 @@ func setupCassandraAuthAndUser(t testing.TB, resource *dockertest.Resource, pool
479542
}
480543
return nil
481544
}
545+
546+
// setupRedisAuthAndUser configures a Redis container by creating a user with permissions.
547+
func setupRedisAuthAndUser(t testing.TB, resource *dockertest.Resource, pool *dockertest.Pool, config *redisConfig) error {
548+
t.Helper()
549+
t.Log("Configuring Redis authentication and user permissions...")
550+
551+
err := exec.Command(
552+
"docker", "exec", config.NetworkAlias, "redis-cli",
553+
"ACL", "SETUSER", config.User, "on", fmt.Sprintf(">%s", config.Password), "+@read", "+@write", "allkeys",
554+
).Run()
555+
if err != nil {
556+
return err
557+
}
558+
559+
return nil
560+
}

0 commit comments

Comments
 (0)