mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
redis driver for blob cache information and metadb (#2865)
* feat: add redis cache support https://github.com/project-zot/zot/pull/2005 Fixes https://github.com/project-zot/zot/issues/2004 * feat: add redis cache support Currently, we have dynamoDB as the remote shared cache but ideal only for the cloud use case. For on-prem use case, add support for redis. Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com> * feat(redis): added blackbox tests for redis Signed-off-by: Petu Eusebiu <peusebiu@cisco.com> * feat(redis): dummy implementation of MetaDB interface for redis cache Signed-off-by: Alexei Dodon <adodon@cisco.com> * feat: check validity of driver configuration on metadb instantiation Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat: multiple fixes for redis cache driver implementation - add missing method GetAllBlobs - add redis cache tests, with and without mocking Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): redis implementation for MetaDB Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): use redsync to block concurrent write access to the redis DB Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): update .github/workflows/cluster.yaml to also test redis Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(metadb): add keyPrefix parameter for redis and remove unneeded method meta.Crate() Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): support RedisCluster configuration and add unit tests Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): more tests for redis metadb implementation Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): add more examples and update examples/README.md Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): move option parsing and redis client initialization under pkg/api/config/redis Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * chore(cachedb): move Cache interface to pkg/storage/types Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): reorganize code in pkg/storage/cache.go Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): call redis.SetLogger() with the zot logger as parameter Signed-off-by: Andrei Aaron <aaaron@luxoft.com> * feat(redis): rename pkg/meta/redisdb to pkg/meta/redis Signed-off-by: Andrei Aaron <aaaron@luxoft.com> --------- Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com> Signed-off-by: Petu Eusebiu <peusebiu@cisco.com> Signed-off-by: Alexei Dodon <adodon@cisco.com> Signed-off-by: Andrei Aaron <aaaron@luxoft.com> Co-authored-by: a <a@tuxpa.in> Co-authored-by: Ramkumar Chinchani <rchincha@cisco.com> Co-authored-by: Petu Eusebiu <peusebiu@cisco.com> Co-authored-by: Alexei Dodon <adodon@cisco.com>
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
package rediscfg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"zotregistry.dev/zot/errors"
|
||||
"zotregistry.dev/zot/pkg/log"
|
||||
)
|
||||
|
||||
var once sync.Once //nolint: gochecknoglobals // redis.SetLogger modifies an unprotected global variable
|
||||
|
||||
type redisLogger struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (r redisLogger) Printf(ctx context.Context, format string, v ...interface{}) {
|
||||
r.log.Debug().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func GetRedisClient(redisConfig map[string]interface{}, log log.Logger) (redis.UniversalClient, error) {
|
||||
once.Do(func() { redis.SetLogger(redisLogger{log}) }) // call redis.SetLogger only once
|
||||
|
||||
// go-redis supports connecting via the redis uri specification (more convenient than parameter parsing)
|
||||
// Note failover/Sentinel cannot be configured via URL parsing at the moment
|
||||
if val, ok := redisConfig["url"]; ok {
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: cachedriver %s has invalid value for url", errors.ErrBadConfig, redisConfig)
|
||||
}
|
||||
|
||||
// The cluster URL has additional addresses in query parameters
|
||||
if strings.Count(str, "addr") > 0 {
|
||||
opts, err := redis.ParseClusterURL(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return redis.NewClusterClient(opts), nil
|
||||
}
|
||||
|
||||
opts, err := redis.ParseURL(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return redis.NewClient(opts), nil
|
||||
}
|
||||
|
||||
// URL configuration not provided by the user, we need to initialize UniversalOptions based on the provided parameters
|
||||
opts := ParseRedisUniversalOptions(redisConfig, log)
|
||||
|
||||
return redis.NewUniversalClient(opts), nil
|
||||
}
|
||||
|
||||
func ParseRedisUniversalOptions(redisConfig map[string]interface{}, //nolint: gocyclo
|
||||
log log.Logger,
|
||||
) *redis.UniversalOptions {
|
||||
opts := redis.UniversalOptions{}
|
||||
sanitizedConfig := map[string]interface{}{}
|
||||
|
||||
for key, val := range redisConfig {
|
||||
if key == "password" || key == "sentinel_password" {
|
||||
sanitizedConfig[key] = "******"
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
sanitizedConfig[key] = val
|
||||
}
|
||||
|
||||
log.Info().Interface("redisConfig", sanitizedConfig).Msg("parsing redis universal options")
|
||||
|
||||
if val, ok := getStringSlice(redisConfig, "addr", log); ok {
|
||||
opts.Addrs = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "client_name", false, log); ok {
|
||||
opts.ClientName = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "db", log); ok {
|
||||
opts.DB = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "protocol", log); ok {
|
||||
opts.Protocol = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "username", false, log); ok {
|
||||
opts.Username = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "password", true, log); ok {
|
||||
opts.Password = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "sentinel_username", false, log); ok {
|
||||
opts.SentinelUsername = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "sentinel_password", true, log); ok {
|
||||
opts.SentinelPassword = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "max_retries", log); ok {
|
||||
opts.MaxRetries = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "min_retry_backoff", log); ok {
|
||||
opts.MinRetryBackoff = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "max_retry_backoff", log); ok {
|
||||
opts.MaxRetryBackoff = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "dial_timeout", log); ok {
|
||||
opts.DialTimeout = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "read_timeout", log); ok {
|
||||
opts.ReadTimeout = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "write_timeout", log); ok {
|
||||
opts.WriteTimeout = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "context_timeout_enabled", log); ok {
|
||||
opts.ContextTimeoutEnabled = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "pool_fifo", log); ok {
|
||||
opts.PoolFIFO = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "pool_size", log); ok {
|
||||
opts.PoolSize = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "pool_timeout", log); ok {
|
||||
opts.PoolTimeout = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "min_idle_conns", log); ok {
|
||||
opts.MinIdleConns = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "max_idle_conns", log); ok {
|
||||
opts.MaxIdleConns = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "max_active_conns", log); ok {
|
||||
opts.MaxActiveConns = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "conn_max_idle_time", log); ok {
|
||||
opts.ConnMaxIdleTime = val
|
||||
}
|
||||
|
||||
if val, ok := getDuration(redisConfig, "conn_max_lifetime", log); ok {
|
||||
opts.ConnMaxLifetime = val
|
||||
}
|
||||
|
||||
if val, ok := getInt(redisConfig, "max_redirects", log); ok {
|
||||
opts.MaxRedirects = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "read_only", log); ok {
|
||||
opts.ReadOnly = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "route_by_latency", log); ok {
|
||||
opts.RouteByLatency = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "route_randomly", log); ok {
|
||||
opts.RouteRandomly = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "master_name", false, log); ok {
|
||||
opts.MasterName = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "disable_identity", log); ok {
|
||||
opts.DisableIndentity = val
|
||||
}
|
||||
|
||||
if val, ok := getString(redisConfig, "identity_suffix", false, log); ok {
|
||||
opts.IdentitySuffix = val
|
||||
}
|
||||
|
||||
if val, ok := getBool(redisConfig, "unstable_resp3", log); ok {
|
||||
opts.UnstableResp3 = val
|
||||
}
|
||||
|
||||
log.Info().Msg("finished parsing redis universal options")
|
||||
|
||||
return &opts
|
||||
}
|
||||
|
||||
func logCastWarning(key string, value interface{}, hideValue bool, log log.Logger) {
|
||||
if hideValue {
|
||||
log.Warn().Str("key", key).Msg("failed to cast parameter to intended type")
|
||||
} else {
|
||||
log.Warn().Str("key", key).Interface("value", value).Msg("failed to cast parameter to intended type")
|
||||
}
|
||||
}
|
||||
|
||||
func getBool(dict map[string]interface{}, key string, log log.Logger) (bool, bool) {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
|
||||
ret, err := cast.ToBoolE(value)
|
||||
if err != nil {
|
||||
logCastWarning(key, value, false, log)
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func getInt(dict map[string]interface{}, key string, log log.Logger) (int, bool) {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
ret, err := cast.ToIntE(value)
|
||||
if err != nil {
|
||||
logCastWarning(key, value, false, log)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func getString(dict map[string]interface{}, key string, hideValue bool, log log.Logger) (string, bool) {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
ret, err := cast.ToStringE(value)
|
||||
if err != nil {
|
||||
logCastWarning(key, value, hideValue, log)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func getStringSlice(dict map[string]interface{}, key string, log log.Logger) ([]string, bool) {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return []string{}, false
|
||||
}
|
||||
|
||||
ret, err := cast.ToStringSliceE(value)
|
||||
if err != nil {
|
||||
logCastWarning(key, value, false, log)
|
||||
|
||||
return []string{}, false
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func getDuration(dict map[string]interface{}, key string, log log.Logger) (time.Duration, bool) {
|
||||
value, ok := dict[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
ret, err := cast.ToDurationE(value)
|
||||
if err != nil {
|
||||
logCastWarning(key, value, false, log)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package rediscfg_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"zotregistry.dev/zot/pkg/api/config"
|
||||
rediscfg "zotregistry.dev/zot/pkg/api/config/redis"
|
||||
"zotregistry.dev/zot/pkg/cli/server"
|
||||
"zotregistry.dev/zot/pkg/log"
|
||||
)
|
||||
|
||||
func TestRedisOptions(t *testing.T) {
|
||||
Convey("Test redis initialization", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
So(log, ShouldNotBeNil)
|
||||
|
||||
Convey("Test redis url parsing", func() {
|
||||
// Errors
|
||||
config := map[string]interface{}{"url": false}
|
||||
|
||||
clientIntf, err := rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(clientIntf, ShouldBeNil)
|
||||
|
||||
config = map[string]interface{}{"url": ""}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(clientIntf, ShouldBeNil)
|
||||
|
||||
config = map[string]interface{}{"url": "qwerty@localhost:6379/1?dial_timeout=5s"}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(clientIntf, ShouldBeNil)
|
||||
|
||||
config = map[string]interface{}{"url": "http://:qwerty@localhost:6379/1?dial_timeout=5s"}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(clientIntf, ShouldBeNil)
|
||||
|
||||
config = map[string]interface{}{"url": "http://localhost:6379/1?addr=host2:6379&addr=host1:6379"}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(clientIntf, ShouldBeNil)
|
||||
|
||||
// Success
|
||||
config = map[string]interface{}{"url": "redis://user:password@localhost:6379/1?dial_timeout=5s"}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok := clientIntf.(*redis.Client)
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
config = map[string]interface{}{"url": "redis://user:password@host1:6379?addr=host2:6379&addr=host1:6379"}
|
||||
|
||||
clientIntf, err = rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok = clientIntf.(*redis.ClusterClient)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test empty redis options from struct successfully", func() {
|
||||
config := map[string]interface{}{}
|
||||
|
||||
// All attributes will have zero values
|
||||
options := rediscfg.ParseRedisUniversalOptions(config, log)
|
||||
So(options, ShouldNotBeNil)
|
||||
So(options.Addrs, ShouldEqual, []string(nil))
|
||||
So(options.DB, ShouldEqual, 0)
|
||||
So(options.MasterName, ShouldEqual, "")
|
||||
So(options.ClientName, ShouldEqual, "")
|
||||
So(options.Protocol, ShouldEqual, 0)
|
||||
So(options.Username, ShouldEqual, "")
|
||||
So(options.Password, ShouldEqual, "")
|
||||
So(options.SentinelUsername, ShouldEqual, "")
|
||||
So(options.SentinelPassword, ShouldEqual, "")
|
||||
So(options.DialTimeout, ShouldEqual, 0)
|
||||
So(options.MaxRetries, ShouldEqual, 0)
|
||||
So(options.MinRetryBackoff, ShouldEqual, 0)
|
||||
So(options.MaxRetryBackoff, ShouldEqual, 0)
|
||||
So(options.ReadTimeout, ShouldEqual, 0)
|
||||
So(options.WriteTimeout, ShouldEqual, 0)
|
||||
So(options.ContextTimeoutEnabled, ShouldEqual, false)
|
||||
So(options.PoolFIFO, ShouldEqual, false)
|
||||
So(options.PoolSize, ShouldEqual, 0)
|
||||
So(options.PoolTimeout, ShouldEqual, 0)
|
||||
So(options.MinIdleConns, ShouldEqual, 0)
|
||||
So(options.MaxIdleConns, ShouldEqual, 0)
|
||||
So(options.MaxActiveConns, ShouldEqual, 0)
|
||||
So(options.ConnMaxIdleTime, ShouldEqual, 0)
|
||||
So(options.ConnMaxLifetime, ShouldEqual, 0)
|
||||
So(options.MaxRedirects, ShouldEqual, 0)
|
||||
So(options.ReadOnly, ShouldEqual, false)
|
||||
So(options.RouteByLatency, ShouldEqual, false)
|
||||
So(options.RouteRandomly, ShouldEqual, false)
|
||||
So(options.DisableIndentity, ShouldEqual, false)
|
||||
So(options.IdentitySuffix, ShouldEqual, "")
|
||||
So(options.UnstableResp3, ShouldEqual, false)
|
||||
|
||||
clientIntf, err := rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok := clientIntf.(*redis.Client)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test redis options from struct successfully", func() {
|
||||
config := map[string]interface{}{
|
||||
"addr": []string{
|
||||
"a.repo:26379",
|
||||
"b.repo:26379",
|
||||
"c.repo:26379",
|
||||
},
|
||||
"db": 1,
|
||||
"master_name": "zotmeta",
|
||||
"client_name": "client",
|
||||
"protocol": 3,
|
||||
"username": "redis",
|
||||
"password": "**secret**",
|
||||
"sentinel_username": "sentinel",
|
||||
"sentinel_password": "**secret**",
|
||||
"dial_timeout": 5 * time.Second,
|
||||
"max_retries": 5,
|
||||
"min_retry_backoff": 1 * time.Second,
|
||||
"max_retry_backoff": 3 * time.Second,
|
||||
"read_timeout": 1 * time.Second,
|
||||
"write_timeout": 1 * time.Second,
|
||||
"context_timeout_enabled": true,
|
||||
"pool_fifo": false,
|
||||
"pool_size": 2,
|
||||
"pool_timeout": 10 * time.Second,
|
||||
"min_idle_conns": 1,
|
||||
"max_idle_conns": 2,
|
||||
"max_active_conns": 3,
|
||||
"conn_max_idle_time": 20 * time.Second,
|
||||
"conn_max_lifetime": 50 * time.Second,
|
||||
"max_redirects": 3,
|
||||
"read_only": true,
|
||||
"route_by_latency": false,
|
||||
"route_randomly": true,
|
||||
"disable_identity": false,
|
||||
"identity_suffix": "test",
|
||||
"unstable_resp3": true,
|
||||
}
|
||||
|
||||
// All attribute values are taken from config
|
||||
options := rediscfg.ParseRedisUniversalOptions(config, log)
|
||||
So(options, ShouldNotBeNil)
|
||||
So(options.Addrs, ShouldEqual, []string{"a.repo:26379", "b.repo:26379", "c.repo:26379"})
|
||||
So(options.DB, ShouldEqual, 1)
|
||||
So(options.MasterName, ShouldEqual, "zotmeta")
|
||||
So(options.ClientName, ShouldEqual, "client")
|
||||
So(options.Protocol, ShouldEqual, 3)
|
||||
So(options.Username, ShouldEqual, "redis")
|
||||
So(options.Password, ShouldEqual, "**secret**")
|
||||
So(options.SentinelUsername, ShouldEqual, "sentinel")
|
||||
So(options.SentinelPassword, ShouldEqual, "**secret**")
|
||||
So(options.DialTimeout, ShouldEqual, 5*time.Second)
|
||||
So(options.MaxRetries, ShouldEqual, 5)
|
||||
So(options.MinRetryBackoff, ShouldEqual, 1*time.Second)
|
||||
So(options.MaxRetryBackoff, ShouldEqual, 3*time.Second)
|
||||
So(options.ReadTimeout, ShouldEqual, 1*time.Second)
|
||||
So(options.WriteTimeout, ShouldEqual, 1*time.Second)
|
||||
So(options.ContextTimeoutEnabled, ShouldEqual, true)
|
||||
So(options.PoolFIFO, ShouldEqual, false)
|
||||
So(options.PoolSize, ShouldEqual, 2)
|
||||
So(options.PoolTimeout, ShouldEqual, 10*time.Second)
|
||||
So(options.MinIdleConns, ShouldEqual, 1)
|
||||
So(options.MaxIdleConns, ShouldEqual, 2)
|
||||
So(options.MaxActiveConns, ShouldEqual, 3)
|
||||
So(options.ConnMaxIdleTime, ShouldEqual, 20*time.Second)
|
||||
So(options.ConnMaxLifetime, ShouldEqual, 50*time.Second)
|
||||
So(options.MaxRedirects, ShouldEqual, 3)
|
||||
So(options.ReadOnly, ShouldEqual, true)
|
||||
So(options.RouteByLatency, ShouldEqual, false)
|
||||
So(options.RouteRandomly, ShouldEqual, true)
|
||||
So(options.DisableIndentity, ShouldEqual, false)
|
||||
So(options.IdentitySuffix, ShouldEqual, "test")
|
||||
So(options.UnstableResp3, ShouldEqual, true)
|
||||
|
||||
clientIntf, err := rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok := clientIntf.(*redis.Client)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test redis options from struct with warnings", func() {
|
||||
config := map[string]interface{}{
|
||||
"addr": map[string]int{},
|
||||
"db": "somestring",
|
||||
"master_name": map[string]int{},
|
||||
"client_name": map[string]int{},
|
||||
"protocol": "somestring",
|
||||
"username": map[string]int{},
|
||||
"password": map[string]int{},
|
||||
"sentinel_username": map[string]int{},
|
||||
"sentinel_password": map[string]int{},
|
||||
"dial_timeout": "somestring",
|
||||
"max_retries": "somestring",
|
||||
"min_retry_backoff": "somestring",
|
||||
"max_retry_backoff": "somestring",
|
||||
"read_timeout": false,
|
||||
"write_timeout": true,
|
||||
"context_timeout_enabled": "somestring",
|
||||
"pool_fifo": "somestring",
|
||||
"pool_size": "somestring",
|
||||
"pool_timeout": "somestring",
|
||||
"min_idle_conns": map[string]int{},
|
||||
"max_idle_conns": map[string]int{},
|
||||
"max_active_conns": "somestring",
|
||||
"conn_max_idle_time": "somestring",
|
||||
"conn_max_lifetime": "somestring",
|
||||
"max_redirects": map[string]int{},
|
||||
"read_only": map[string]int{},
|
||||
"route_by_latency": "somestring",
|
||||
"route_randomly": map[string]int{},
|
||||
"disable_identity": "somestring",
|
||||
"identity_suffix": map[string]int{},
|
||||
"unstable_resp3": "somestring",
|
||||
}
|
||||
|
||||
// All attributes remain with default values
|
||||
options := rediscfg.ParseRedisUniversalOptions(config, log)
|
||||
So(options, ShouldNotBeNil)
|
||||
So(options.Addrs, ShouldEqual, []string(nil))
|
||||
So(options.DB, ShouldEqual, 0)
|
||||
So(options.MasterName, ShouldEqual, "")
|
||||
So(options.ClientName, ShouldEqual, "")
|
||||
So(options.Protocol, ShouldEqual, 0)
|
||||
So(options.Username, ShouldEqual, "")
|
||||
So(options.Password, ShouldEqual, "")
|
||||
So(options.SentinelUsername, ShouldEqual, "")
|
||||
So(options.SentinelPassword, ShouldEqual, "")
|
||||
So(options.DialTimeout, ShouldEqual, 0)
|
||||
So(options.MaxRetries, ShouldEqual, 0)
|
||||
So(options.MinRetryBackoff, ShouldEqual, 0)
|
||||
So(options.MaxRetryBackoff, ShouldEqual, 0)
|
||||
So(options.ReadTimeout, ShouldEqual, 0)
|
||||
So(options.WriteTimeout, ShouldEqual, 0)
|
||||
So(options.ContextTimeoutEnabled, ShouldEqual, false)
|
||||
So(options.PoolFIFO, ShouldEqual, false)
|
||||
So(options.PoolSize, ShouldEqual, 0)
|
||||
So(options.PoolTimeout, ShouldEqual, 0)
|
||||
So(options.MinIdleConns, ShouldEqual, 0)
|
||||
So(options.MaxIdleConns, ShouldEqual, 0)
|
||||
So(options.MaxActiveConns, ShouldEqual, 0)
|
||||
So(options.ConnMaxIdleTime, ShouldEqual, 0)
|
||||
So(options.ConnMaxLifetime, ShouldEqual, 0)
|
||||
So(options.MaxRedirects, ShouldEqual, 0)
|
||||
So(options.ReadOnly, ShouldEqual, false)
|
||||
So(options.RouteByLatency, ShouldEqual, false)
|
||||
So(options.RouteRandomly, ShouldEqual, false)
|
||||
So(options.DisableIndentity, ShouldEqual, false)
|
||||
So(options.IdentitySuffix, ShouldEqual, "")
|
||||
So(options.UnstableResp3, ShouldEqual, false)
|
||||
|
||||
clientIntf, err := rediscfg.GetRedisClient(config, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok := clientIntf.(*redis.Client)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test redis options from json", func(c C) {
|
||||
fileContent := []byte(`{
|
||||
"distSpecVersion": "1.1.0",
|
||||
"storage": {
|
||||
"remoteCache": true,
|
||||
"cacheDriver": {
|
||||
"name": "redis",
|
||||
"addr": [
|
||||
"a.repo:26379",
|
||||
"b.repo:26379",
|
||||
"c.repo:26379"
|
||||
],
|
||||
"db": 1,
|
||||
"master_name": "zotmeta",
|
||||
"username": "redis",
|
||||
"password": "**secret**",
|
||||
"dial_timeout": "5s"
|
||||
},
|
||||
"commit": false,
|
||||
"dedupe": false,
|
||||
"gc": true,
|
||||
"rootDirectory": "/data/zot-cache/dev"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`)
|
||||
|
||||
dir := t.TempDir()
|
||||
configPath := path.Join(dir, "test-config.json")
|
||||
|
||||
err := os.WriteFile(configPath, fileContent, 0o600)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
conf := config.New()
|
||||
err = server.LoadConfiguration(conf, configPath)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
options := rediscfg.ParseRedisUniversalOptions(conf.Storage.CacheDriver, log)
|
||||
So(options, ShouldNotBeNil)
|
||||
So(options.Addrs, ShouldEqual, []string{"a.repo:26379", "b.repo:26379", "c.repo:26379"})
|
||||
So(options.DB, ShouldEqual, 1)
|
||||
So(options.MasterName, ShouldEqual, "zotmeta")
|
||||
So(options.Username, ShouldEqual, "redis")
|
||||
So(options.Password, ShouldEqual, "**secret**")
|
||||
So(options.DialTimeout, ShouldEqual, 5*time.Second)
|
||||
|
||||
clientIntf, err := rediscfg.GetRedisClient(conf.Storage.CacheDriver, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(clientIntf, ShouldNotBeNil)
|
||||
|
||||
_, ok := clientIntf.(*redis.Client)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
+122
-4
@@ -26,6 +26,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/google/go-github/v62/github"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
@@ -154,6 +155,55 @@ func TestCreateCacheDatabaseDriver(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(driver, ShouldBeNil)
|
||||
})
|
||||
Convey("Test CreateCacheDatabaseDriver redisdb", t, func() {
|
||||
miniRedis := miniredis.RunT(t)
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
dir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.Storage.RootDirectory = dir
|
||||
conf.Storage.Dedupe = true
|
||||
conf.Storage.RemoteCache = true
|
||||
|
||||
// test error on invalid redis client config
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "redis",
|
||||
"url": false,
|
||||
}
|
||||
|
||||
driver, err := storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(driver, ShouldBeNil)
|
||||
|
||||
// test valid redis client config
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "redis",
|
||||
"url": "redis://" + miniRedis.Addr(),
|
||||
}
|
||||
|
||||
// test initialization for S3 storage
|
||||
conf.Storage.StorageDriver = map[string]interface{}{
|
||||
"name": "s3",
|
||||
"rootdirectory": "/zot",
|
||||
"url": "us-east-2",
|
||||
}
|
||||
|
||||
driver, err = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(driver, ShouldNotBeNil)
|
||||
So(driver.Name(), ShouldEqual, "redis")
|
||||
So(driver.UsesRelativePaths(), ShouldEqual, false)
|
||||
|
||||
// test initialization for local storage
|
||||
conf.Storage.StorageDriver = nil
|
||||
|
||||
driver, err = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(driver, ShouldNotBeNil)
|
||||
So(driver.Name(), ShouldEqual, "redis")
|
||||
So(driver.UsesRelativePaths(), ShouldEqual, true)
|
||||
})
|
||||
tskip.SkipDynamo(t)
|
||||
tskip.SkipS3(t)
|
||||
Convey("Test CreateCacheDatabaseDriver dynamodb", t, func() {
|
||||
@@ -226,7 +276,7 @@ func TestCreateCacheDatabaseDriver(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateMetaDBDriver(t *testing.T) {
|
||||
Convey("Test CreateCacheDatabaseDriver dynamo", t, func() {
|
||||
Convey("Test create MetaDB dynamo", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
dir := t.TempDir()
|
||||
conf := config.New()
|
||||
@@ -253,11 +303,26 @@ func TestCreateMetaDBDriver(t *testing.T) {
|
||||
"userdatatablename": "UserDatatable",
|
||||
}
|
||||
|
||||
metaDB, err := meta.New(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(metaDB, ShouldBeNil)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "dynamodb",
|
||||
"endpoint": "http://localhost:4566",
|
||||
"region": "us-east-2",
|
||||
"cachetablename": "BlobTable",
|
||||
"repometatablename": "RepoMetadataTable",
|
||||
"imageMetaTablename": "ZotImageMetaTable",
|
||||
"repoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
|
||||
"userdatatablename": "UserDatatable",
|
||||
}
|
||||
|
||||
testFunc := func() { _, _ = meta.New(conf.Storage.StorageConfig, log) }
|
||||
So(testFunc, ShouldPanic)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "dummy",
|
||||
"name": "dynamodb",
|
||||
"endpoint": "http://localhost:4566",
|
||||
"region": "us-east-2",
|
||||
"cachetablename": "",
|
||||
@@ -272,7 +337,7 @@ func TestCreateMetaDBDriver(t *testing.T) {
|
||||
So(testFunc, ShouldPanic)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "dummy",
|
||||
"name": "dynamodb",
|
||||
"endpoint": "http://localhost:4566",
|
||||
"region": "us-east-2",
|
||||
"cachetablename": "test",
|
||||
@@ -288,7 +353,60 @@ func TestCreateMetaDBDriver(t *testing.T) {
|
||||
So(testFunc, ShouldNotPanic)
|
||||
})
|
||||
|
||||
Convey("Test CreateCacheDatabaseDriver bolt", t, func() {
|
||||
Convey("Test create MetaDB redis", t, func() {
|
||||
miniRedis := miniredis.RunT(t)
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
dir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.Storage.RootDirectory = dir
|
||||
conf.Storage.Dedupe = true
|
||||
conf.Storage.RemoteCache = true
|
||||
conf.Storage.StorageDriver = map[string]interface{}{
|
||||
"name": "s3",
|
||||
"rootdirectory": "/zot",
|
||||
"region": "us-east-2",
|
||||
"bucket": "zot-storage",
|
||||
"secure": true,
|
||||
"skipverify": false,
|
||||
}
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "dummy",
|
||||
}
|
||||
|
||||
metaDB, err := meta.New(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(metaDB, ShouldBeNil)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "redis",
|
||||
}
|
||||
|
||||
metaDB, err = meta.New(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(metaDB, ShouldBeNil)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "redis",
|
||||
"url": "url",
|
||||
}
|
||||
|
||||
metaDB, err = meta.New(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(metaDB, ShouldBeNil)
|
||||
|
||||
conf.Storage.CacheDriver = map[string]interface{}{
|
||||
"name": "redis",
|
||||
"url": "redis://" + miniRedis.Addr(),
|
||||
}
|
||||
|
||||
metaDB, err = meta.New(conf.Storage.StorageConfig, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(metaDB, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test create MetaDB bolt", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
dir := t.TempDir()
|
||||
conf := config.New()
|
||||
|
||||
Reference in New Issue
Block a user