Skip to content

Conversation

andsens
Copy link
Contributor

@andsens andsens commented Mar 19, 2024

Less code more features! :-D

This PR replaces the custom Redis config struct with UniversalOptions from go-redis, which implicitly adds support for both redis sentinel and redis clustering.

I have two failing tests that I can't quite figure out how to fix:

    --- FAIL: TestConfigSuite/TestMarshalRoundtrip (0.00s)
    --- FAIL: TestConfigSuite/TestParseInvalidVersion (0.00s)
Associated stack trace
    suite.go:87: test panicked: cannot marshal type: func(context.Context, string, string) (net.Conn, error)
        goroutine 18 [running]:
        runtime/debug.Stack()
        	/usr/lib/go-1.21/src/runtime/debug/stack.go:24 +0x5e
        github.com/stretchr/testify/suite.failOnPanic(0xc0002b8820, {0x8ebe40, 0xc0002c5850})
        	/home/aim/Workspace/forks/distribution/vendor/github.com/stretchr/testify/suite/suite.go:87 +0x39
        github.com/stretchr/testify/suite.Run.func1.1()
        	/home/aim/Workspace/forks/distribution/vendor/github.com/stretchr/testify/suite/suite.go:183 +0x24c
        panic({0x8ebe40?, 0xc0002c5850?})
        	/usr/lib/go-1.21/src/runtime/panic.go:914 +0x21f
        gopkg.in/yaml%2ev2.handleErr(0xc0001513c8)
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/yaml.go:249 +0x6d
        panic({0x8ebe40?, 0xc0002c5850?})
        	/usr/lib/go-1.21/src/runtime/panic.go:914 +0x21f
        gopkg.in/yaml%2ev2.(*encoder).marshal(0xc000220b00, {0x0, 0x0}, {0x913e00?, 0xc0002de618?, 0x0?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:183 +0xa34
        gopkg.in/yaml%2ev2.(*encoder).structv.func1()
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:226 +0x1fc
        gopkg.in/yaml%2ev2.(*encoder).mappingv(0xc000220b00, {0x0?, 0x0}, 0xc000150c58)
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:256 +0x14a
        gopkg.in/yaml%2ev2.(*encoder).structv(0xc000220b00, {0x0, 0x0}, {0x9778a0?, 0xc0002de5e8?, 0xc0002de6c8?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:213 +0xe5
        gopkg.in/yaml%2ev2.(*encoder).marshal(0xc000220b00, {0x0, 0x0}, {0x9778a0?, 0xc0002de5e8?, 0x91b7e0?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:160 +0x945
        gopkg.in/yaml%2ev2.(*encoder).structv.func1()
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:226 +0x1fc
        gopkg.in/yaml%2ev2.(*encoder).mappingv(0xc000220b00, {0x0?, 0x0}, 0xc00013d028)
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:256 +0x14a
        gopkg.in/yaml%2ev2.(*encoder).structv(0xc000220b00, {0x0, 0x0}, {0x96d600?, 0xc0002de400?, 0x18?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:213 +0xe5
        gopkg.in/yaml%2ev2.(*encoder).marshal(0xc000220b00, {0x0, 0x0}, {0x96d600?, 0xc0002de400?, 0x96d7c0?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:160 +0x945
        gopkg.in/yaml%2ev2.(*encoder).marshal(0xc000220b00, {0x0, 0x0}, {0x8dcb80?, 0xc0002de400?, 0xc00013d348?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:154 +0x74b
        gopkg.in/yaml%2ev2.(*encoder).marshalDoc(0xc000220b00, {0x0, 0x0}, {0x8dcb80?, 0xc0002de400?, 0xc00013d3c8?})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/encode.go:93 +0xcb
        gopkg.in/yaml%2ev2.Marshal({0x8dcb80, 0xc0002de400})
        	/home/aim/Workspace/forks/distribution/vendor/gopkg.in/yaml.v2/yaml.go:203 +0x319
        github.com/distribution/distribution/v3/configuration.(*ConfigSuite).TestParseInvalidVersion(0xc00007e4b0)
        	/home/aim/Workspace/forks/distribution/configuration/configuration_test.go:397 +0xfa
        reflect.Value.call({0xc00007de80?, 0xc0000550b8?, 0x0?}, {0x99472c, 0x4}, {0xc000065e70, 0x1, 0xc000065d78?})
        	/usr/lib/go-1.21/src/reflect/value.go:596 +0xce7
        reflect.Value.Call({0xc00007de80?, 0xc0000550b8?, 0xc00007e4b0?}, {0xc000065e70?, 0x5214cd?, 0xc69e78?})
        	/usr/lib/go-1.21/src/reflect/value.go:380 +0xb9
        github.com/stretchr/testify/suite.Run.func1(0xc0002b8820)
        	/home/aim/Workspace/forks/distribution/vendor/github.com/stretchr/testify/suite/suite.go:197 +0x467
        testing.tRunner(0xc0002b8820, 0xc00019c630)
        	/usr/lib/go-1.21/src/testing/testing.go:1595 +0xff
        created by testing.(*T).Run in goroutine 6
        	/usr/lib/go-1.21/src/testing/testing.go:1648 +0x3ad

As v3 is still in alpha I was hoping that there would still be time for this to make it into the release.

@andsens andsens changed the title Replace custom Redis config struct with go-redis UniversalOptions Replace custom Redis config struct with go-redis UniversalOptions (adds sentinel & cluster support) Mar 19, 2024
@andsens andsens force-pushed the ft-redis-universal-options branch from 4431aa2 to b09dbb1 Compare March 19, 2024 10:07
@Jamstah
Copy link
Collaborator

Jamstah commented Mar 19, 2024

Copy link
Member

@milosgajdos milosgajdos left a comment

Choose a reason for hiding this comment

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

The problem with this change is, it's tying the code to a specific Go module, which I'm not entirely sure is a good idea.

@andsens andsens force-pushed the ft-redis-universal-options branch from b09dbb1 to d8a1f01 Compare March 19, 2024 10:16
@andsens
Copy link
Contributor Author

andsens commented Mar 19, 2024

@Jamstah done.
@milosgajdos, I understand the issue. I mean, we could just replicate the entire struct I guess? In practice the options struct seems to be quite stable and well thought out. The tests will catch any incompatible changes when bumping and then we can decide from there what to do about it, rather than doing a bunch of work that might not be necessary.

p.s.: I figured out the test failure: The marshaller is trying to serialize the Dialer function I believe, but I'm not familiar enough with the rest of the code to determine how to fix that.

@andsens
Copy link
Contributor Author

andsens commented Mar 19, 2024

Also, it looks like redis.tls.enabled had no effect previously? I don't see it used anywhere...

@andsens andsens force-pushed the ft-redis-universal-options branch from d8a1f01 to c6615dc Compare March 19, 2024 10:48
@andsens
Copy link
Contributor Author

andsens commented Mar 19, 2024

@milosgajdos, replicating the struct actually fixes the marshalling sooo 🤷‍♂️ fixed :-D

@andsens andsens force-pushed the ft-redis-universal-options branch from c6615dc to f74a981 Compare March 19, 2024 11:05
@Jamstah
Copy link
Collaborator

Jamstah commented Mar 19, 2024

We're already using the go module to provide the client itself, so why not also use it to provide the config struct?

If we ever change go module it would need rewriting anyway.

I like the idea of matching our config to be more "standard".

@andsens
Copy link
Contributor Author

andsens commented Mar 19, 2024

@Jamstah I agree. In that case the marshalling (or the test) needs some work though. I don't know how to exclude the functions that are referenced in those configs, any ideas?

@andsens
Copy link
Contributor Author

andsens commented Apr 2, 2024

*ping after the holidays. I would love if we could figure out how to progress with this.

Copy link
Member

@milosgajdos milosgajdos left a comment

Choose a reason for hiding this comment

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

We are still missing doc updates here:

## `redis`
```yaml
redis:
addr: localhost:6379
password: asecret
db: 0
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
tls:
enabled: false
```
Declare parameters for constructing the `redis` connections. Registry instances
may use the Redis instance for several applications. Currently, it caches
information about immutable blobs. Most of the `redis` options control
how the registry connects to the `redis` instance. You can control the pool's
behavior with the [pool](#pool) subsection. Additionally, you can control
TLS connection settings with the [tls](#tls) subsection (in-transit encryption).
You should configure Redis with the **allkeys-lru** eviction policy, because the
registry does not set an expiration value on keys.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `addr` | yes | The address (host and port) of the Redis instance. |
| `password`| no | A password used to authenticate to the Redis instance.|
| `db` | no | The name of the database to use for each connection. |
| `dialtimeout` | no | The timeout for connecting to the Redis instance. |
| `readtimeout` | no | The timeout for reading from the Redis instance. |
| `writetimeout` | no | The timeout for writing to the Redis instance. |
### `pool`
```yaml
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
```
Use these settings to configure the behavior of the Redis connection pool.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `maxidle` | no | The maximum number of idle connections in the pool. |
| `maxactive`| no | The maximum number of connections which can be open before blocking a connection request. |
| `idletimeout`| no | How long to wait before closing inactive connections. |
### `tls`
```yaml
tls:
enabled: false
```
Use these settings to configure Redis TLS.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------- |
| `enabled` | no | Whether or not to use TLS in-transit. |

Also, it looks like redis.tls.enabled had no effect previously? I don't see it used anywhere...

Yes, that was an oversight on my side when switching to the new module. There is an issue someone opened recently: #4284

My other comment is...there is now a boat load of new config items. What are they initialized to when omitted from the config file? Is not having a config value for each of them going to make the user confused? I guess what I'm trying to say is: what's the behaviour like when they're initialized to their default Go vals when omitted.


// DB specifies the database to connect to on the redis instance.
DB int `yaml:"db,omitempty"`
Protocol int
Copy link
Member

Choose a reason for hiding this comment

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

What does Protocol stand for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Protocol 2 or 3. Use the version to negotiate RESP version with redis-server. Default is 3.

It seems to have been removed in the latest v9.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably would be useful to carry the comments from here https://github.com/redis/go-redis/blob/v9.1.0/options.go#L31-L139 into this struct as well in case questions like these come up again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's not the UniversalOptions struct though :-/
I carried over all the comments from that one.

Again, it's probably a lot smarter to simply use that struct directly. The indirection just adds a lot of confusion. If the go-redis struct is referenced directly you know there isn't any tomfoolery happening.

@andsens
Copy link
Contributor Author

andsens commented Apr 8, 2024

Here are the structs with all the options specified:

I'm starting to think that using the redis-go UniversalOptions struct directly is the better option here. It cuts down on maintenance and documentation. We can simply link out to e.g. https://pkg.go.dev/github.com/go-redis/redis/v9#UniversalOptions.

All that is needed for that is figuring out how to get around the marshalling issue.

@milosgajdos
Copy link
Member

I'm starting to think that using the redis-go UniversalOptions struct directly is the better option here. It cuts down on maintenance and documentation. We can simply link out to e.g. https://pkg.go.dev/github.com/go-redis/redis/v9#UniversalOptions.

I think that might be the best course of action should we decide to go forth with this.

@andsens andsens force-pushed the ft-redis-universal-options branch from f74a981 to 2fa441f Compare April 12, 2024 09:45
@milosgajdos
Copy link
Member

@andsens can you sign your commit, please

@andsens
Copy link
Contributor Author

andsens commented Apr 12, 2024

@milosgajdos ah crap, sry. I reverted to the previous version and forgot to amend. Will do when I get back to my work PC. In the meantime, what do we do about the failed marshalling of the config?
It's not used anywhere except during testing. I guess it's a useful feature for debugging.

@milosgajdos
Copy link
Member

There is some silly reflection stuff in configuration package to enable the ENV var shortcuts via cli...we should try fixing it

@andsens andsens force-pushed the ft-redis-universal-options branch from 2fa441f to 51037d3 Compare April 15, 2024 08:47
@andsens
Copy link
Contributor Author

andsens commented May 25, 2024

*bump
Is there any chance someone can help me with this? I don't have the expertise to get those two tests to pass.
We have been using this version in production for 2 months now, and it's been working like a charm. The failover on redis with sentinel happens seamlessly and through it all parts of distribution are now HA.

@milosgajdos
Copy link
Member

Yeah, the problem is UniversalOptions contain functions as fields 🫠 which is ....not great in...like...any Go code for that matter.

The configuration marshaler naturally barfs its guts encountering them....this also complicates Registry's env var configuration -- which frankly I wouldn't mind ditching, but many are relying on passing config via env vars.

I need to have a think about this...

@@ -130,20 +131,14 @@ var configStruct = Configuration{
Enabled: true,
},
},
Redis: Redis{
Addr: "localhost:6379",
Redis: redis.UniversalOptions{
Copy link
Member

Choose a reason for hiding this comment

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

FYI: this isn't Go fmt-ed

This was referenced May 27, 2024
@milosgajdos milosgajdos added this to the Registry/3.0.0 milestone May 28, 2024
@milosgajdos
Copy link
Member

@andsens how are you getting on with this PR? If you don't have any bandwidth I might look into it this week.

@andsens
Copy link
Contributor Author

andsens commented Jun 12, 2024

@milosgajdos could you?
I have actually made the time for it now, but after trying to get unmarshalling to work so the tests pass I have to admit that I have way too little experience with Go to understand how that entire show is run.
I can see that we also need to introduce some defaults. Lifting them from options.go in go-redis seems like the most sensible thing to do (a bit annoying they don't have it for the UniversalOptions variant).

@milosgajdos
Copy link
Member

So I tried taking this over and pushing a patch to your branch but don't have the perms, @andsens

I think the issue is your branch is on an ORG repo rather than a PERSONAL repo so I can't push there...because "GH reasons"

If you have time you could apply the patch below which works and fixes all the tests.

TL;DR: I had to add an UnmarshalYAML func that handles unmarshalling because we have a nested imported struct there...so...can't easily add yaml struct tags...anyways, the patch is below

Toggle me!
diff --git configuration/configuration.go configuration/configuration.go
index 26216078..884552da 100644
--- configuration/configuration.go
+++ configuration/configuration.go
@@ -175,7 +175,7 @@ type Configuration struct {
 	Notifications Notifications `yaml:"notifications,omitempty"`
 
 	// Redis configures the redis pool available to the registry webapp.
-	Redis redis.UniversalOptions `yaml:"redis,omitempty"`
+	Redis Redis `yaml:"redis,omitempty"`
 
 	Health  Health  `yaml:"health,omitempty"`
 	Catalog Catalog `yaml:"catalog,omitempty"`
@@ -652,3 +652,124 @@ func Parse(rd io.Reader) (*Configuration, error) {
 
 	return config, nil
 }
+
+type Redis struct {
+	redis.UniversalOptions
+}
+
+func (c Redis) MarshalYAML() (interface{}, error) {
+	fields := make(map[string]interface{})
+
+	val := reflect.ValueOf(c.UniversalOptions)
+	typ := val.Type()
+
+	for i := 0; i < val.NumField(); i++ {
+		field := typ.Field(i)
+		fieldValue := val.Field(i)
+
+		// ignore imports and funcs
+		if field.PkgPath != "" || fieldValue.Kind() == reflect.Func {
+			continue
+		}
+
+		fields[strings.ToLower(field.Name)] = fieldValue.Interface()
+	}
+
+	return fields, nil
+}
+
+func (c *Redis) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var fields map[string]interface{}
+	err := unmarshal(&fields)
+	if err != nil {
+		return err
+	}
+
+	val := reflect.ValueOf(&c.UniversalOptions).Elem()
+	typ := val.Type()
+
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+		fieldName := strings.ToLower(field.Name)
+
+		if value, ok := fields[fieldName]; ok {
+			fieldValue := val.Field(i)
+			if fieldValue.CanSet() {
+				switch field.Type {
+				case reflect.TypeOf(time.Duration(0)):
+					durationStr, ok := value.(string)
+					if !ok {
+						return fmt.Errorf("invalid duration value for field: %s", fieldName)
+					}
+					duration, err := time.ParseDuration(durationStr)
+					if err != nil {
+						return fmt.Errorf("failed to parse duration for field: %s, error: %v", fieldName, err)
+					}
+					fieldValue.Set(reflect.ValueOf(duration))
+				default:
+					if err := setFieldValue(fieldValue, value); err != nil {
+						return fmt.Errorf("failed to set value for field: %s, error: %v", fieldName, err)
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func setFieldValue(field reflect.Value, value interface{}) error {
+	if value == nil {
+		return nil
+	}
+
+	switch field.Kind() {
+	case reflect.String:
+		stringValue, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("failed to convert value to string")
+		}
+		field.SetString(stringValue)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		intValue, ok := value.(int)
+		if !ok {
+			return fmt.Errorf("failed to convert value to integer")
+		}
+		field.SetInt(int64(intValue))
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		uintValue, ok := value.(uint)
+		if !ok {
+			return fmt.Errorf("failed to convert value to unsigned integer")
+		}
+		field.SetUint(uint64(uintValue))
+	case reflect.Float32, reflect.Float64:
+		floatValue, ok := value.(float64)
+		if !ok {
+			return fmt.Errorf("failed to convert value to float")
+		}
+		field.SetFloat(floatValue)
+	case reflect.Bool:
+		boolValue, ok := value.(bool)
+		if !ok {
+			return fmt.Errorf("failed to convert value to boolean")
+		}
+		field.SetBool(boolValue)
+	case reflect.Slice:
+		slice := reflect.MakeSlice(field.Type(), 0, 0)
+		valueSlice, ok := value.([]interface{})
+		if !ok {
+			return fmt.Errorf("failed to convert value to slice")
+		}
+		for _, item := range valueSlice {
+			sliceValue := reflect.New(field.Type().Elem()).Elem()
+			if err := setFieldValue(sliceValue, item); err != nil {
+				return err
+			}
+			slice = reflect.Append(slice, sliceValue)
+		}
+		field.Set(slice)
+	default:
+		return fmt.Errorf("unsupported field type: %v", field.Type())
+	}
+	return nil
+}
diff --git configuration/configuration_test.go configuration/configuration_test.go
index 3a1e0d09..b7018807 100644
--- configuration/configuration_test.go
+++ configuration/configuration_test.go
@@ -131,17 +131,19 @@ var configStruct = Configuration{
 			Enabled: true,
 		},
 	},
-	Redis: redis.UniversalOptions{
-		Addrs:     []string{"localhost:6379"},
-		Username: "alice",
-		Password: "123456",
-		DB:       1,
-		MaxIdleConns: 16,
-		PoolSize: 64,
-		ConnMaxIdleTime: time.Second * 300,
-		DialTimeout:  time.Millisecond * 10,
-		ReadTimeout:  time.Millisecond * 10,
-		WriteTimeout: time.Millisecond * 10,
+	Redis: Redis{
+		redis.UniversalOptions{
+			Addrs:           []string{"localhost:6379"},
+			Username:        "alice",
+			Password:        "123456",
+			DB:              1,
+			MaxIdleConns:    16,
+			PoolSize:        64,
+			ConnMaxIdleTime: time.Second * 300,
+			DialTimeout:     time.Millisecond * 10,
+			ReadTimeout:     time.Millisecond * 10,
+			WriteTimeout:    time.Millisecond * 10,
+		},
 	},
 }
 
@@ -187,7 +189,7 @@ http:
 redis:
   addrs: [localhost:6379]
   username: alice
-  password: 123456
+  password: "123456"
   db: 1
   maxidleconns: 16
   poolsize: 64
@@ -263,7 +265,7 @@ func (suite *ConfigSuite) TestParseSimple() {
 func (suite *ConfigSuite) TestParseInmemory() {
 	suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
 	suite.expectedConfig.Log.Fields = nil
-	suite.expectedConfig.Redis = redis.UniversalOptions{}
+	suite.expectedConfig.Redis = Redis{}
 
 	config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
 	suite.Require().NoError(err)
@@ -283,7 +285,7 @@ func (suite *ConfigSuite) TestParseIncomplete() {
 	suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
 	suite.expectedConfig.Notifications = Notifications{}
 	suite.expectedConfig.HTTP.Headers = nil
-	suite.expectedConfig.Redis = redis.UniversalOptions{}
+	suite.expectedConfig.Redis = Redis{}
 
 	// Note: this also tests that REGISTRY_STORAGE and
 	// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
diff --git registry/handlers/app.go registry/handlers/app.go
index 373d28c1..e108dc2e 100644
--- registry/handlers/app.go
+++ registry/handlers/app.go
@@ -492,7 +492,7 @@ func (app *App) configureRedis(cfg *configuration.Configuration) {
 		return
 	}
 
-	app.redis = app.createPool(cfg.Redis)
+	app.redis = app.createPool(cfg.Redis.UniversalOptions)
 
 	// Enable metrics instrumentation.
 	if err := redisotel.InstrumentMetrics(app.redis); err != nil {
@@ -518,8 +518,8 @@ func (app *App) createPool(cfg redis.UniversalOptions) redis.UniversalClient {
 	cfg.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
 		res := cn.Ping(ctx)
 		return res.Err()
-	};
-	return redis.NewUniversalClient(&cfg);
+	}
+	return redis.NewUniversalClient(&cfg)
 }
 
 // configureLogHook prepares logging hook parameters.

If you grab that diff save it in a file like foo.patch and then do

git apply foo.patch

That should get your tests and lining issues sorted 🤞

@andsens andsens force-pushed the ft-redis-universal-options branch from 51037d3 to 750d983 Compare June 14, 2024 08:13
Huge help from @milosgajdos who figured out how to do the entire
marshalling/unmarshalling for the configs

Signed-off-by: Anders Ingemann <aim@orbit.online>
@andsens andsens force-pushed the ft-redis-universal-options branch from 750d983 to b63cbb3 Compare June 14, 2024 08:31
@andsens
Copy link
Contributor Author

andsens commented Jun 14, 2024

@milosgajdos done! Thank you so much for helping out.
I have also updated the documentation to reflect the changes.
Though, the tls options are still a big questionmark. They have the same issue regarding function fields. At least you might be able to enable it I think and use CAs installed on the host for verifying the chain (not tested).

@milosgajdos
Copy link
Member

Though, the tls options are still a big questionmark. They have the same issue regarding function fields.

Arrgh, I see. Hmm, maybe we can "hack" around it and simply introduce an additional TLS substruct and then create the tls.Config and pass it to the options instance...let me think about this...We're getting there! 😄

We also update the Redis TLS config initialization in the app.

Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
@milosgajdos
Copy link
Member

I've finally added support for custom TLS config in Redis @andsens 🤞

PTAL @Jamstah @squizzi @thaJeztah

@andsens
Copy link
Contributor Author

andsens commented Jun 28, 2024

Awesome work @milosgajdos! Looking forward to ditching our fork and running the official server again :-D

@thaJeztah
Copy link
Member

Some very quick thoughts (overall this looks like a nice PR);

When setting these options through the Go API, users are currently required to (as we do in our tests) instance a redis.UniversalOptions to set the config. This also means that if distribution would update its dependency to go-redis/v10, it would be a breaking change, and from a go module (SemVer) perspective, would require distribution/distribution to do a major version update (v4).

Thinking out loud here;

We can could consider creating an alias for redis.UniversalOptions;

type RedisOptions = redis.UniversalOptions

Consumers of the config package can use that alias to construct a config (and we can recommend them to do so); IF we would have to update the dependency to go-redis/v10, and if that update would not introduce a breaking change in the UniversalOptions struct, the alias would take care of keeping our module backward compatible; users of the config package would continue to use config.Redis (which now just is an alias to a different (v10) module).

On a slight note; Redis.UniversalOptions is a bit of a mouthful; wondering if we should create a shorter name for it;

type RedisOptions = redis.UniversalOptions

type Redis struct {
	Options RedisOptions `yaml:",inline"`
	TLS                    struct {
		Certificate string   `yaml:"certificate,omitempty"`
		Key         string   `yaml:"key,omitempty"`
		ClientCAs   []string `yaml:"clientcas,omitempty"`
	} `yaml:"tls,omitempty"`
}

Finally; perhaps we can define a TLSOptions type instead of the struct; I think that would make it easier to construct a config from code;

type RedisOptions = redis.UniversalOptions

type TLSOptions struct {
	Certificate string   `yaml:"certificate,omitempty"`
	Key         string   `yaml:"key,omitempty"`
	ClientCAs   []string `yaml:"clientcas,omitempty"`
}

type Redis struct {
	Options RedisOptions `yaml:",inline"`
	TLS     TLSOptions   `yaml:"tls,omitempty"`
}

@milosgajdos
Copy link
Member

milosgajdos commented Jun 30, 2024

Thanks for the feedback, @thaJeztah . This (your suggestions) makes a lot of sense indeed if we want to maintain b/w compact, which I agree we should strive for as much as possible.

The only thing I'm wondering about is the TLSOptions type. It'd have to be RedisTLSOptions IF we were to rip it out into a dedicated type. Mostly due to the following reasons:

  • consistency sake (there is an anonymous struct pattern established in the configuration package, see the TLS inside the HTTP config:
    TLS struct {
    // Certificate specifies the path to an x509 certificate file to
    // be used for TLS.
    Certificate string `yaml:"certificate,omitempty"`
    // Key specifies the path to the x509 key file, which should
    // contain the private portion for the file specified in
    // Certificate.
    Key string `yaml:"key,omitempty"`
    // Specifies the CA certs for client authentication
    // A file may contain multiple CA certificates encoded as PEM
    ClientCAs []string `yaml:"clientcas,omitempty"`
    // Specifies the lowest TLS version allowed
    MinimumTLS string `yaml:"minimumtls,omitempty"`
    // Specifies a list of cipher suites allowed
    CipherSuites []string `yaml:"ciphersuites,omitempty"`
    // LetsEncrypt is used to configuration setting up TLS through
    // Let's Encrypt instead of manually specifying certificate and
    // key. If a TLS certificate is specified, the Let's Encrypt
    // section will not be used.
    LetsEncrypt struct {
    // CacheFile specifies cache file to use for lets encrypt
    // certificates and keys.
    CacheFile string `yaml:"cachefile,omitempty"`
    // Email is the email to use during Let's Encrypt registration
    Email string `yaml:"email,omitempty"`
    // Hosts specifies the hosts which are allowed to obtain Let's
    // Encrypt certificates.
    Hosts []string `yaml:"hosts,omitempty"`
    // DirectoryURL points to the CA directory endpoint.
    // If empty, LetsEncrypt is used.
    DirectoryURL string `yaml:"directoryurl,omitempty"`
    } `yaml:"letsencrypt,omitempty"`
    } `yaml:"tls,omitempty"`
    ; we can't easily replace that because it contains other things we dot necessarily need in Redis config per see)
  • It could be consumed as configuration.TLSOptions which makes it seem more generic than it is (see the mention of HTTP.TLS earlier); unless, I suppose, we "massage" that TLS config somehow 🤔

But all in all Im fully onboard with the notion of a type alias. I think it makes a lot of sense, indeed. Thanks for bringing this up!

Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
@milosgajdos milosgajdos requested a review from thaJeztah June 30, 2024 10:21
@milosgajdos
Copy link
Member

PTAL @thaJeztah

@Jamstah
Copy link
Collaborator

Jamstah commented Jul 2, 2024

Seems like a sensible change to me, good spot on giving us options for backwards compatibility

Copy link
Collaborator

@davidspek davidspek left a comment

Choose a reason for hiding this comment

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

LGTM!

Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
@milosgajdos milosgajdos merged commit 306f4ff into distribution:main Jul 4, 2024
@andsens
Copy link
Contributor Author

andsens commented Jul 4, 2024

🥳 awesome!
Thanks for all the work you put into this @milosgajdos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants