mirror of
https://github.com/distribution/distribution.git
synced 2025-07-16 16:32:30 +00:00
feat: extract redis config to separate struct
Signed-off-by: Mateusz Urbanek <mateusz.urbanek.98@gmail.com>
This commit is contained in:
parent
e028a30ecd
commit
fcb2deac0b
@ -8,8 +8,6 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Configuration is a versioned registry configuration, intended to be provided by a yaml file, and
|
||||
@ -833,10 +831,105 @@ func Parse(rd io.Reader) (*Configuration, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// RedisOptions represents the configuration options for Redis, which are
|
||||
// provided by the redis package. This struct can be used to configure the
|
||||
// connection to Redis in a universal (clustered or standalone) setup.
|
||||
type RedisOptions = redis.UniversalOptions
|
||||
// RedisOptions represents the configuration options for Redis. This struct can be used
|
||||
// to configure the connection to Redis in a universal (clustered or standalone) setup.
|
||||
type RedisOptions struct {
|
||||
// Addrs is either a single address or a seed list of host:port addresses
|
||||
// of cluster/sentinel nodes.
|
||||
Addrs []string `yaml:"addrs,omitempty"`
|
||||
|
||||
// ClientName will execute the `CLIENT SETNAME ClientName` command for each connection.
|
||||
ClientName string `yaml:"clientname,omitempty"`
|
||||
|
||||
// DB is the database to be selected after connecting to the server.
|
||||
// Only applicable to single-node and failover clients.
|
||||
DB int `yaml:"db,omitempty"`
|
||||
|
||||
// Protocol specifies the Redis protocol version to use.
|
||||
Protocol int `yaml:"protocol,omitempty"`
|
||||
|
||||
// Username for authentication (used with ACLs).
|
||||
Username string `yaml:"username,omitempty"`
|
||||
|
||||
// Password for authentication.
|
||||
Password string `yaml:"password,omitempty"`
|
||||
|
||||
// SentinelUsername is the username for Sentinel authentication.
|
||||
SentinelUsername string `yaml:"sentinelusername,omitempty"`
|
||||
|
||||
// SentinelPassword is the password for Sentinel authentication.
|
||||
SentinelPassword string `yaml:"sentinelpassword,omitempty"`
|
||||
|
||||
// MaxRetries is the maximum number of retries before giving up.
|
||||
MaxRetries int `yaml:"maxretries,omitempty"`
|
||||
|
||||
// MinRetryBackoff is the minimum backoff between each retry.
|
||||
MinRetryBackoff time.Duration `yaml:"minretrybackoff,omitempty"`
|
||||
|
||||
// MaxRetryBackoff is the maximum backoff between each retry.
|
||||
MaxRetryBackoff time.Duration `yaml:"maxretrybackoff,omitempty"`
|
||||
|
||||
// DialTimeout is the timeout for establishing new connections.
|
||||
DialTimeout time.Duration `yaml:"dialtimeout,omitempty"`
|
||||
|
||||
// ReadTimeout is the timeout for reading a single command reply.
|
||||
ReadTimeout time.Duration `yaml:"readtimeout,omitempty"`
|
||||
|
||||
// WriteTimeout is the timeout for writing a single command.
|
||||
WriteTimeout time.Duration `yaml:"writetimeout,omitempty"`
|
||||
|
||||
// ContextTimeoutEnabled enables wrapping operations with a context timeout.
|
||||
ContextTimeoutEnabled bool `yaml:"contexttimeoutenabled,omitempty"`
|
||||
|
||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default is LIFO).
|
||||
PoolFIFO bool `yaml:"poolfifo,omitempty"`
|
||||
|
||||
// PoolSize is the maximum number of socket connections.
|
||||
PoolSize int `yaml:"poolsize,omitempty"`
|
||||
|
||||
// PoolTimeout is the amount of time a client waits for a connection if all are busy.
|
||||
PoolTimeout time.Duration `yaml:"pooltimeout,omitempty"`
|
||||
|
||||
// MinIdleConns is the minimum number of idle connections maintained in the pool.
|
||||
MinIdleConns int `yaml:"minidleconns,omitempty"`
|
||||
|
||||
// MaxIdleConns is the maximum number of idle connections.
|
||||
MaxIdleConns int `yaml:"maxidleconns,omitempty"`
|
||||
|
||||
// MaxActiveConns is the maximum number of active connections (cluster mode only).
|
||||
MaxActiveConns int `yaml:"maxactiveconns,omitempty"`
|
||||
|
||||
// ConnMaxIdleTime is the maximum amount of time a connection can be idle.
|
||||
ConnMaxIdleTime time.Duration `yaml:"connmaxidletime,omitempty"`
|
||||
|
||||
// ConnMaxLifetime is the maximum lifetime of a connection.
|
||||
ConnMaxLifetime time.Duration `yaml:"connmaxlifetime,omitempty"`
|
||||
|
||||
// MaxRedirects is the maximum number of redirects to follow in cluster mode.
|
||||
MaxRedirects int `yaml:"maxredirects,omitempty"`
|
||||
|
||||
// ReadOnly enables read-only mode for cluster clients.
|
||||
ReadOnly bool `yaml:"readonly,omitempty"`
|
||||
|
||||
// RouteByLatency routes commands to the closest node based on latency.
|
||||
RouteByLatency bool `yaml:"routebylatency,omitempty"`
|
||||
|
||||
// RouteRandomly routes commands randomly among eligible nodes.
|
||||
RouteRandomly bool `yaml:"routerandomly,omitempty"`
|
||||
|
||||
// MasterName is the Sentinel master name.
|
||||
// Only applicable for failover clients.
|
||||
MasterName string `yaml:"mastername,omitempty"`
|
||||
|
||||
// DisableIdentity disables the CLIENT SETINFO command on connect.
|
||||
DisableIdentity bool `yaml:"disableidentity,omitempty"`
|
||||
|
||||
// IdentitySuffix is an optional suffix for CLIENT SETINFO.
|
||||
IdentitySuffix string `yaml:"identitysuffix,omitempty"`
|
||||
|
||||
// UnstableResp3 enables RESP3 features that are not finalized yet.
|
||||
UnstableResp3 bool `yaml:"unstableresp3,omitempty"`
|
||||
}
|
||||
|
||||
// RedisTLSOptions configures the TLS (Transport Layer Security) settings for
|
||||
// Redis connections, allowing secure communication over the network.
|
||||
@ -867,162 +960,6 @@ type Redis struct {
|
||||
TLS RedisTLSOptions `yaml:"tls,omitempty"`
|
||||
}
|
||||
|
||||
func (c Redis) MarshalYAML() (interface{}, error) {
|
||||
fields := make(map[string]interface{})
|
||||
|
||||
val := reflect.ValueOf(c.Options)
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fieldValue := val.Field(i)
|
||||
|
||||
// ignore funcs fields in redis.UniversalOptions
|
||||
if fieldValue.Kind() == reflect.Func {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[strings.ToLower(field.Name)] = fieldValue.Interface()
|
||||
}
|
||||
|
||||
// Add TLS fields if they're not empty
|
||||
if c.TLS.Certificate != "" || c.TLS.Key != "" || len(c.TLS.ClientCAs) > 0 {
|
||||
fields["tls"] = c.TLS
|
||||
}
|
||||
|
||||
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.Options).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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle TLS fields
|
||||
if tlsData, ok := fields["tls"]; ok {
|
||||
tlsMap, ok := tlsData.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid TLS data structure")
|
||||
}
|
||||
|
||||
if cert, ok := tlsMap["certificate"]; ok {
|
||||
var isString bool
|
||||
c.TLS.Certificate, isString = cert.(string)
|
||||
if !isString {
|
||||
return fmt.Errorf("Redis TLS certificate must be a string")
|
||||
}
|
||||
}
|
||||
if key, ok := tlsMap["key"]; ok {
|
||||
var isString bool
|
||||
c.TLS.Key, isString = key.(string)
|
||||
if !isString {
|
||||
return fmt.Errorf("Redis TLS (private) key must be a string")
|
||||
}
|
||||
}
|
||||
if cas, ok := tlsMap["clientcas"]; ok {
|
||||
caList, ok := cas.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid clientcas data structure")
|
||||
}
|
||||
for _, ca := range caList {
|
||||
if caStr, ok := ca.(string); ok {
|
||||
c.TLS.ClientCAs = append(c.TLS.ClientCAs, caStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const (
|
||||
ClientAuthRequestClientCert = "request-client-cert"
|
||||
ClientAuthRequireAnyClientCert = "require-any-client-cert"
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
@ -76,7 +75,7 @@ var configStruct = Configuration{
|
||||
},
|
||||
},
|
||||
Redis: Redis{
|
||||
Options: redis.UniversalOptions{
|
||||
Options: RedisOptions{
|
||||
Addrs: []string{"localhost:6379"},
|
||||
Username: "alice",
|
||||
Password: "123456",
|
||||
|
@ -536,6 +536,40 @@ func (app *App) configureRedis(cfg *configuration.Configuration) {
|
||||
return
|
||||
}
|
||||
|
||||
opts := redis.UniversalOptions{
|
||||
Addrs: cfg.Redis.Options.Addrs,
|
||||
ClientName: cfg.Redis.Options.ClientName,
|
||||
DB: cfg.Redis.Options.DB,
|
||||
Protocol: cfg.Redis.Options.Protocol,
|
||||
Username: cfg.Redis.Options.Username,
|
||||
Password: cfg.Redis.Options.Password,
|
||||
SentinelUsername: cfg.Redis.Options.SentinelUsername,
|
||||
SentinelPassword: cfg.Redis.Options.SentinelPassword,
|
||||
MaxRetries: cfg.Redis.Options.MaxRetries,
|
||||
MinRetryBackoff: cfg.Redis.Options.MinRetryBackoff,
|
||||
MaxRetryBackoff: cfg.Redis.Options.MaxRetryBackoff,
|
||||
DialTimeout: cfg.Redis.Options.DialTimeout,
|
||||
ReadTimeout: cfg.Redis.Options.ReadTimeout,
|
||||
WriteTimeout: cfg.Redis.Options.WriteTimeout,
|
||||
ContextTimeoutEnabled: cfg.Redis.Options.ContextTimeoutEnabled,
|
||||
PoolFIFO: cfg.Redis.Options.PoolFIFO,
|
||||
PoolSize: cfg.Redis.Options.PoolSize,
|
||||
PoolTimeout: cfg.Redis.Options.PoolTimeout,
|
||||
MinIdleConns: cfg.Redis.Options.MinIdleConns,
|
||||
MaxIdleConns: cfg.Redis.Options.MaxIdleConns,
|
||||
MaxActiveConns: cfg.Redis.Options.MaxActiveConns,
|
||||
ConnMaxIdleTime: cfg.Redis.Options.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: cfg.Redis.Options.ConnMaxLifetime,
|
||||
MaxRedirects: cfg.Redis.Options.MaxRedirects,
|
||||
ReadOnly: cfg.Redis.Options.ReadOnly,
|
||||
RouteByLatency: cfg.Redis.Options.RouteByLatency,
|
||||
RouteRandomly: cfg.Redis.Options.RouteRandomly,
|
||||
MasterName: cfg.Redis.Options.MasterName,
|
||||
DisableIdentity: cfg.Redis.Options.DisableIdentity,
|
||||
IdentitySuffix: cfg.Redis.Options.IdentitySuffix,
|
||||
UnstableResp3: cfg.Redis.Options.UnstableResp3,
|
||||
}
|
||||
|
||||
// redis TLS config
|
||||
if cfg.Redis.TLS.Certificate != "" || cfg.Redis.TLS.Key != "" {
|
||||
var err error
|
||||
@ -562,10 +596,10 @@ func (app *App) configureRedis(cfg *configuration.Configuration) {
|
||||
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
tlsConf.ClientCAs = pool
|
||||
}
|
||||
cfg.Redis.Options.TLSConfig = tlsConf
|
||||
opts.TLSConfig = tlsConf
|
||||
}
|
||||
|
||||
app.redis = app.createPool(cfg.Redis.Options)
|
||||
app.redis = app.createPool(opts)
|
||||
|
||||
// Enable metrics instrumentation.
|
||||
if err := redisotel.InstrumentMetrics(app.redis); err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user