Skip to content
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

Support using redis urls to construct the redis client #1994

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config.dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,9 @@ ShutdownTimeout = 60
# Env override: ATHENS_ETCD_ENDPOINTS
Endpoints = "localhost:2379,localhost:22379,localhost:32379"
[SingleFlight.Redis]
# Endpoint is the redis endpoint for a SingleFlight lock.
# Endpoint is the redis endpoint for a SingleFlight lock. Should be either a host:port
# pair or redis url such as:
# redis[s]://user:password@127.0.0.1:6379/0?protocol=3
# TODO(marwan): enable multiple endpoints for redis clusters.
# Env override: ATHENS_REDIS_ENDPOINT
Endpoint = "127.0.0.1:6379"
Expand Down
16 changes: 16 additions & 0 deletions docs/content/configuration/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,22 @@ You can also optionally specify a password to connect to the redis server with
# Env override: ATHENS_REDIS_PASSWORD
Password = ""

Connecting to Redis via a [redis url](https://github.com/redis/redis-specifications/blob/master/uri/redis.txt) is also
supported:

SingleFlightType = "redis"

[SingleFlight]
[SingleFlight.Redis]
# Endpoint is the redis endpoint for the single flight mechanism
# Env override: ATHENS_REDIS_ENDPOINT
# Note, if TLS is required use rediss:// instead.
Endpoint = "redis://user:password@127.0.0.1:6379:6379/0?protocol=3"

If the redis url is invalid or cannot be parsed, Athens will fall back to treating `Endpoint` as if it were
a normal `host:port` pair. If a password is supplied in the redis url, in addition to being provided in the `Password`
configuration option, the two values must match otherwise Athens will fail to start.

##### Customizing lock configurations:
If you would like to customize the distributed lock options then you can optionally override the default lock config to better suit your use-case:

Expand Down
46 changes: 40 additions & 6 deletions pkg/stash/with_redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,56 @@ type RedisLogger interface {
Printf(ctx context.Context, format string, v ...any)
}

var errPasswordsDoNotMatch = goerrors.New("a redis url was parsed that contained a password but the configuration also defined a specific redis password, please ensure these values match or use only one of them")

// getRedisClientOptions takes an endpoint and password and returns *redis.Options to use
// with the redis client. endpoint may be a redis url or host:port combination. If a redis
// url is used and a password is also used this function checks to make sure the parsed redis
// url has produced the same password. Preferably, one should use EITHER a redis url or a host:port
// combination w/password but not both. More information on the redis url structure can be found
// here: https://github.com/redis/redis-specifications/blob/master/uri/redis.txt
func getRedisClientOptions(endpoint, password string) (*redis.Options, error) {
// Try parsing the endpoint as a redis url first. The redis library does not define
// a specific error when parsing the url so we fall back on the old config here
// which passed in arguments.
options, err := redis.ParseURL(endpoint)
if err != nil {
return &redis.Options{ //nolint:nilerr // We are specifically falling back here and ignoring the error on purpose.
Network: "tcp",
Addr: endpoint,
Password: password,
}, nil
}

// Ensure the password is either empty or that it matches the password
// parsed from the url into redis.Options. This ensures that if the
// config supplies the password but a redis url doesn't the behavior
// is clear vs. failing later on at the time of the first connection
// with an 'invalid password' like error.
if password != "" && options.Password != password {
return nil, errPasswordsDoNotMatch
}

return options, nil
}

// WithRedisLock returns a distributed singleflight
// using a redis cluster. If it cannot connect, it will return an error.
func WithRedisLock(l RedisLogger, endpoint, password string, checker storage.Checker, lockConfig *config.RedisLockConfig) (Wrapper, error) {
redis.SetLogger(l)

const op errors.Op = "stash.WithRedisLock"
client := redis.NewClient(&redis.Options{
Network: "tcp",
Addr: endpoint,
Password: password,
})
_, err := client.Ping(context.Background()).Result()

options, err := getRedisClientOptions(endpoint, password)
if err != nil {
return nil, errors.E(op, err)
}

client := redis.NewClient(options)
if _, err := client.Ping(context.Background()).Result(); err != nil {
return nil, errors.E(op, err)
}

lockOptions, err := lockOptionsFromConfig(lockConfig)
if err != nil {
return nil, errors.E(op, err)
Expand Down
69 changes: 69 additions & 0 deletions pkg/stash/with_redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"testing"
"time"

"github.com/go-redis/redis/v8"
"github.com/gomods/athens/pkg/config"
"github.com/gomods/athens/pkg/errors"
"github.com/gomods/athens/pkg/storage"
"github.com/gomods/athens/pkg/storage/mem"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -120,6 +122,73 @@ func TestWithRedisLockWithWrongPassword(t *testing.T) {
}
}

type getRedisClientOptionsFacet struct {
endpoint string
password string
options *redis.Options
err error
}

func Test_getRedisClientOptions(t *testing.T) {
facets := []*getRedisClientOptionsFacet{
{
endpoint: "127.0.0.1:6379",
options: &redis.Options{
Addr: "127.0.0.1:6379",
},
},
{
endpoint: "127.0.0.1:6379",
password: "1234",
options: &redis.Options{
Addr: "127.0.0.1:6379",
Password: "1234",
},
},
{
endpoint: "rediss://username:password@127.0.0.1:6379",
password: "1234", // Ignored because password was parsed
err: errors.E("stash.WithRedisLock", errPasswordsDoNotMatch),
},
{
endpoint: "rediss://username:password@127.0.0.1:6379",
password: "1234", // Ignored because password was parsed
err: errors.E("stash.WithRedisLock", errPasswordsDoNotMatch),
},
}

for i, facet := range facets {
options, err := getRedisClientOptions(facet.endpoint, facet.password)
if err != nil && facet.err == nil {
t.Errorf("Facet %d: no error produced", i)
continue
}
if facet.err != nil {
if err == nil {
t.Errorf("Facet %d: no error produced", i)
} else {
if err.Error() != facet.err.Error() {
t.Errorf("Facet %d: expected %q, got %q", i, facet.err, err)
}
}
}

if err != nil {
continue
}
if facet.options.Addr != options.Addr {
t.Errorf("Facet %d: Expected Addr %q, got %q", i, facet.options.Addr, options.Addr)
}
if facet.options.Username != options.Username {
t.Errorf("Facet %d: Expected Username %q, got %q", i, facet.options.Username, options.Username)
}
if facet.options.Password != options.Password {
t.Errorf("Facet %d: Expected Password %q, got %q", i, facet.options.Password, options.Password)
}

}
}

// mockRedisStasher is like mockStasher
// but leverages in memory storage
// so that redis can determine
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ WORKDIR /tmp

# Install Helm
ENV HELM_VERSION=2.13.0
RUN curl -sLO https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
RUN curl -sLO https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
matt0x6F marked this conversation as resolved.
Show resolved Hide resolved
tar -zxvf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
mv linux-amd64/helm /usr/local/bin/

# Install a tiny azure client
ENV AZCLI_VERSION=v0.3.1
ENV AZCLI_VERSION=v0.3.2
RUN curl -sLo /usr/local/bin/az https://github.com/carolynvs/az-cli/releases/download/$AZCLI_VERSION/az-linux-amd64 && \
chmod +x /usr/local/bin/az

Expand Down