diff --git a/bcs-common/common/redisclient/client.go b/bcs-common/common/redisclient/client.go new file mode 100644 index 0000000000..5f35f1c791 --- /dev/null +++ b/bcs-common/common/redisclient/client.go @@ -0,0 +1,57 @@ +package redisclient + +import ( + "context" + "fmt" + "time" + + "github.com/alicebob/miniredis" + "github.com/go-redis/redis/v8" +) + +type RedisMode string + +const ( + SingleMode RedisMode = "single" // Single mode + SentinelMode RedisMode = "sentinel" // Sentinel mode + ClusterMode RedisMode = "cluster" // Cluster mode +) + +type Client interface { + // GetCli return the underlying Redis client + GetCli() redis.UniversalClient + // Ping checks the Redis server connection + Ping(ctx context.Context) (string, error) + Get(ctx context.Context, key string) (string, error) + Exists(ctx context.Context, key ...string) (int64, error) + Set(ctx context.Context, key string, value interface{}, duration time.Duration) (string, error) + SetEX(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, error) + SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) + Del(ctx context.Context, key string) (int64, error) + Expire(ctx context.Context, key string, duration time.Duration) (bool, error) +} + +// NewClient creates a Redis client based on the configuration for different deployment modes +func NewClient(config Config) (Client, error) { + switch config.Mode { + case SingleMode: + return NewSingleClient(config) + case SentinelMode: + return NewSentinelClient(config) + case ClusterMode: + return NewClusterClient(config) + } + return nil, fmt.Errorf("invalid config mode: %s", config.Mode) +} + +// NewTestClient creates a Redis client for unit testing +func NewTestClient() (Client, error) { + mr, err := miniredis.Run() + if err != nil { + return nil, err + } + client := redis.NewClient(&redis.Options{ + Addr: mr.Addr(), + }) + return &SingleClient{cli: client}, nil +} diff --git a/bcs-common/common/redisclient/cluster.go b/bcs-common/common/redisclient/cluster.go new file mode 100644 index 0000000000..b263ff873d --- /dev/null +++ b/bcs-common/common/redisclient/cluster.go @@ -0,0 +1,64 @@ +package redisclient + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" +) + +// ClusterClient Redis client for cluster mode +type ClusterClient struct { + cli *redis.ClusterClient +} + +// NewClusterClient init ClusterClient from config +func NewClusterClient(config Config) (*ClusterClient, error) { + cli := redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: config.Addrs, + Password: config.Password, + DialTimeout: config.DialTimeout * time.Second, + ReadTimeout: config.ReadTimeout * time.Second, + WriteTimeout: config.WriteTimeout * time.Second, + PoolSize: config.PoolSize, + MinIdleConns: config.MinIdleConns, + IdleTimeout: config.IdleTimeout * time.Second, + }) + return &ClusterClient{cli: cli}, nil +} + +func (c *ClusterClient) GetCli() redis.UniversalClient { + return c.cli +} + +func (c *ClusterClient) Ping(ctx context.Context) (string, error) { + return c.cli.Ping(ctx).Result() +} + +func (c *ClusterClient) Get(ctx context.Context, key string) (string, error) { + return c.cli.Get(ctx, key).Result() +} + +func (c *ClusterClient) Exists(ctx context.Context, key ...string) (int64, error) { + return c.cli.Exists(ctx, key...).Result() +} + +func (c *ClusterClient) Set(ctx context.Context, key string, value interface{}, duration time.Duration) (string, error) { + return c.cli.Set(ctx, key, value, duration).Result() +} + +func (c *ClusterClient) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) { + return c.cli.SetNX(ctx, key, value, expiration).Result() +} + +func (c *ClusterClient) SetEX(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, error) { + return c.cli.SetEX(ctx, key, value, expiration).Result() +} + +func (c *ClusterClient) Del(ctx context.Context, key string) (int64, error) { + return c.cli.Del(ctx, key).Result() +} + +func (c *ClusterClient) Expire(ctx context.Context, key string, duration time.Duration) (bool, error) { + return c.cli.Expire(ctx, key, duration).Result() +} diff --git a/bcs-common/common/redisclient/cluster_test.go b/bcs-common/common/redisclient/cluster_test.go new file mode 100644 index 0000000000..31273f241e --- /dev/null +++ b/bcs-common/common/redisclient/cluster_test.go @@ -0,0 +1,58 @@ +package redisclient + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// setupClusterClient function for initializing Redis ClusterClient +func setupClusterClient(t *testing.T) *ClusterClient { + config := Config{ + Mode: ClusterMode, + Addrs: []string{"127.0.0.1:7021", "127.0.0.1:7022", "127.0.0.1:7023"}, + } + client, err := NewClusterClient(config) + assert.NoError(t, err) + assert.NotNil(t, client) + return client +} + +// TestClusterPing tests ClusterClient connectivity +func TestClusterPing(t *testing.T) { + client := setupClusterClient(t) + result, err := client.GetCli().Ping(context.TODO()).Result() + assert.NoError(t, err) + assert.Equal(t, "PONG", result) +} + +// TestClusterClient tests ClusterClient basic functionality +func TestClusterClient(t *testing.T) { + client := setupClusterClient(t) + ctx := context.Background() + + // Test Set operation + _, err := client.Set(ctx, "key1", "value1", 10*time.Second) + assert.NoError(t, err) + + // Test Get operation + val, err := client.Get(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", val) + + // Test Exists operation + exists, err := client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(1), exists) + + // Test Del operation + _, err = client.Del(ctx, "key1") + assert.NoError(t, err) + + // Test if the key has been deleted + exists, err = client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(0), exists) +} diff --git a/bcs-common/common/redisclient/config.go b/bcs-common/common/redisclient/config.go new file mode 100644 index 0000000000..1c387c265c --- /dev/null +++ b/bcs-common/common/redisclient/config.go @@ -0,0 +1,20 @@ +package redisclient + +import "time" + +// Config contains the configuration required to initialize a Redis client +type Config struct { + Addrs []string // List of nodes (addresses) + MasterName string // Master node name in Sentinel mode + Password string // Password + DB int // Database index in single node mode + Mode RedisMode // Redis mode: single, sentinel, or cluster + + // Options configs + DialTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration + PoolSize int + MinIdleConns int + IdleTimeout time.Duration +} diff --git a/bcs-common/common/redisclient/sentinel.go b/bcs-common/common/redisclient/sentinel.go new file mode 100644 index 0000000000..f0c7908f07 --- /dev/null +++ b/bcs-common/common/redisclient/sentinel.go @@ -0,0 +1,70 @@ +package redisclient + +import ( + "context" + "errors" + "time" + + "github.com/go-redis/redis/v8" +) + +// SentinelClient Redis client for sentinel mode +type SentinelClient struct { + cli *redis.Client +} + +// NewSentinelClient init SentinelClient from config +func NewSentinelClient(config Config) (*SentinelClient, error) { + if config.Mode != SentinelMode { + return nil, errors.New("redis mode not supported") + } + cli := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: config.MasterName, + SentinelAddrs: config.Addrs, + Password: config.Password, + DB: config.DB, + DialTimeout: config.DialTimeout, + ReadTimeout: config.ReadTimeout, + WriteTimeout: config.WriteTimeout, + PoolSize: config.PoolSize, + MinIdleConns: config.MinIdleConns, + IdleTimeout: config.IdleTimeout, + }) + return &SentinelClient{cli: cli}, nil +} + +func (c *SentinelClient) GetCli() redis.UniversalClient { + return c.cli +} + +func (c *SentinelClient) Ping(ctx context.Context) (string, error) { + return c.cli.Ping(ctx).Result() +} + +func (c *SentinelClient) Set(ctx context.Context, key string, value interface{}, duration time.Duration) (string, error) { + return c.cli.Set(ctx, key, value, duration).Result() +} + +func (c *SentinelClient) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) { + return c.cli.SetNX(ctx, key, value, expiration).Result() +} + +func (c *SentinelClient) Get(ctx context.Context, key string) (string, error) { + return c.cli.Get(ctx, key).Result() +} + +func (c *SentinelClient) Exists(ctx context.Context, key ...string) (int64, error) { + return c.cli.Exists(ctx, key...).Result() +} + +func (c *SentinelClient) SetEX(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, error) { + return c.cli.SetEX(ctx, key, value, expiration).Result() +} + +func (c *SentinelClient) Del(ctx context.Context, key string) (int64, error) { + return c.cli.Del(ctx, key).Result() +} + +func (c *SentinelClient) Expire(ctx context.Context, key string, duration time.Duration) (bool, error) { + return c.cli.Expire(ctx, key, duration).Result() +} diff --git a/bcs-common/common/redisclient/sentinel_test.go b/bcs-common/common/redisclient/sentinel_test.go new file mode 100644 index 0000000000..a94f4e88ee --- /dev/null +++ b/bcs-common/common/redisclient/sentinel_test.go @@ -0,0 +1,61 @@ +package redisclient + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// setupClusterClient function for initializing Redis SentinelClient +func setupSentinel(t *testing.T) *SentinelClient { + config := Config{ + Mode: SentinelMode, + Addrs: []string{"127.0.0.1:5001"}, // Sentinel addresses + MasterName: "mymaster", // Master name + DB: 0, + Password: "", + } + client, err := NewSentinelClient(config) + assert.NoError(t, err) + assert.NotNil(t, client) + return client +} + +// TestSentinelClientPing tests SentinelClient connectivity +func TestSentinelClientPing(t *testing.T) { + client := setupSentinel(t) + result, err := client.GetCli().Ping(context.TODO()).Result() + assert.NoError(t, err) + assert.Equal(t, "PONG", result) +} + +// TestSentinelClient tests SentinelClient basic functionality +func TestSentinelClient(t *testing.T) { + client := setupSentinel(t) + ctx := context.Background() + + // Test Set operation + _, err := client.Set(ctx, "key1", "value1", 10*time.Second) + assert.NoError(t, err) + + // Test Get operation + val, err := client.Get(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", val) + + // Test Exists operation + exists, err := client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(1), exists) + + // Test Del operation + _, err = client.Del(ctx, "key1") + assert.NoError(t, err) + + // Test if the key has been deleted + exists, err = client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(0), exists) +} diff --git a/bcs-common/common/redisclient/single.go b/bcs-common/common/redisclient/single.go new file mode 100644 index 0000000000..303945989b --- /dev/null +++ b/bcs-common/common/redisclient/single.go @@ -0,0 +1,82 @@ +package redisclient + +import ( + "context" + "errors" + "time" + + "github.com/go-redis/redis/v8" +) + +// SingleClient Redis client for single mode +type SingleClient struct { + cli *redis.Client +} + +// NewSingleClient init SingleClient from config +func NewSingleClient(config Config) (*SingleClient, error) { + if config.Mode != SingleMode { + return nil, errors.New("redis mode not supported") + } + if len(config.Addrs) == 0 { + return nil, errors.New("address is empty") + } + cli := redis.NewClient(&redis.Options{ + Addr: config.Addrs[0], + Password: config.Password, + DB: config.DB, + DialTimeout: config.DialTimeout, + ReadTimeout: config.ReadTimeout, + WriteTimeout: config.WriteTimeout, + PoolSize: config.PoolSize, + MinIdleConns: config.MinIdleConns, + IdleTimeout: config.IdleTimeout, + }) + return &SingleClient{cli: cli}, nil +} + +// NewSingleClientFromDSN init SingleClient by dsn +func NewSingleClientFromDSN(dsn string) (*SingleClient, error) { + options, err := redis.ParseURL(dsn) + if err != nil { + return nil, err + } + cli := redis.NewClient(options) + return &SingleClient{cli: cli}, nil +} + +func (c *SingleClient) GetCli() redis.UniversalClient { + return c.cli +} + +func (c *SingleClient) Ping(ctx context.Context) (string, error) { + return c.cli.Ping(ctx).Result() +} + +func (c *SingleClient) Get(ctx context.Context, key string) (string, error) { + return c.cli.Get(ctx, key).Result() +} + +func (c *SingleClient) Set(ctx context.Context, key string, value interface{}, duration time.Duration) (string, error) { + return c.cli.Set(ctx, key, value, duration).Result() +} + +func (c *SingleClient) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) { + return c.cli.SetNX(ctx, key, value, expiration).Result() +} + +func (c *SingleClient) SetEX(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, error) { + return c.cli.SetEX(ctx, key, value, expiration).Result() +} + +func (c *SingleClient) Exists(ctx context.Context, key ...string) (int64, error) { + return c.cli.Exists(ctx, key...).Result() +} + +func (c *SingleClient) Del(ctx context.Context, key string) (int64, error) { + return c.cli.Del(ctx, key).Result() +} + +func (c *SingleClient) Expire(ctx context.Context, key string, duration time.Duration) (bool, error) { + return c.cli.Expire(ctx, key, duration).Result() +} diff --git a/bcs-common/common/redisclient/single_test.go b/bcs-common/common/redisclient/single_test.go new file mode 100644 index 0000000000..2cfb4565a7 --- /dev/null +++ b/bcs-common/common/redisclient/single_test.go @@ -0,0 +1,119 @@ +package redisclient + +import ( + "context" + "testing" + "time" + + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +func setupSingleClient() Client { + // Create Redis single instance configuration + config := Config{ + Mode: SingleMode, + Addrs: []string{"127.0.0.1:6379"}, + DB: 0, + } + + // Initialize Redis client + client, _ := NewClient(config) + return client +} + +// Test for Ping command +func TestPing(t *testing.T) { + client := setupSingleClient() + result, err := client.GetCli().Ping(context.TODO()).Result() + assert.NoError(t, err) + assert.Equal(t, result, "PONG") +} + +// Test basic functionalities of SingleClient +func TestSingleClient(t *testing.T) { + client := setupSingleClient() + assert.NotNil(t, client) + + ctx := context.Background() + + // Test Get operation + _, err := client.Set(ctx, "key1", "value1", 10*time.Second) + assert.NoError(t, err) + + // Test Get operation + val, err := client.Get(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, "value1", val) + + // Test key existence + exists, err := client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(1), exists) + + // Test Del operation + _, err = client.Del(ctx, "key1") + assert.NoError(t, err) + + // Test whether the key is deleted + exists, err = client.Exists(ctx, "key1") + assert.NoError(t, err) + assert.Equal(t, int64(0), exists) + +} + +// Test SetEX and SetNX operations +func TestSingleClientSetEXAndSetNX(t *testing.T) { + client := setupSingleClient() + assert.NotNil(t, client) + + ctx := context.Background() + + // Test SetEX operation by setting a key with expiration time + _, err := client.SetEX(ctx, "key2", "value2", 5*time.Second) + assert.NoError(t, err) + + // Get key2 and verify the value + val, err := client.Get(ctx, "key2") + assert.NoError(t, err) + assert.Equal(t, "value2", val) + + // Confirm that key2 exists in Redis + exists, err := client.Exists(ctx, "key2") + assert.NoError(t, err) + assert.Equal(t, int64(1), exists) + + // Wait for the expiration time and check if the key still exists + time.Sleep(6 * time.Second) + exists, err = client.Exists(ctx, "key2") + assert.NoError(t, err) + assert.Equal(t, int64(0), exists) + + // Test SetNX operation, which sets the key only if it does not exist + success, err := client.SetNX(ctx, "key3", "value3", 10*time.Second) + assert.NoError(t, err) + assert.True(t, success) + + // Try SetNX again, should return false as the key already exists + success, err = client.SetNX(ctx, "key3", "value3", 10*time.Second) + assert.NoError(t, err) + assert.False(t, success) + + // Delete key3 + _, err = client.Del(ctx, "key3") + assert.NoError(t, err) +} + +// Test edge cases, such as retrieving a non-existent key +func TestSingleClientGetNonExistentKey(t *testing.T) { + client := setupSingleClient() + assert.NotNil(t, client) + + ctx := context.Background() + + // Test retrieving a non-existent key, should return an empty string and redis.Nil error + val, err := client.Get(ctx, "nonexistent") + assert.Error(t, err) + assert.Equal(t, redis.Nil, err) + assert.Equal(t, "", val) +} diff --git a/bcs-common/go.mod b/bcs-common/go.mod index e8ef98f499..a7ac29b71c 100644 --- a/bcs-common/go.mod +++ b/bcs-common/go.mod @@ -6,6 +6,7 @@ require ( github.com/Tencent/bk-bcs/bcs-runtime/bcs-k8s/kubernetes/common v0.0.0-20220330120237-0bbed74dcf6d github.com/TencentBlueKing/bk-audit-go-sdk v0.0.6 github.com/TencentBlueKing/iam-go-sdk v0.1.6 + github.com/alicebob/miniredis v2.5.0+incompatible github.com/bitly/go-simplejson v0.5.0 github.com/docker/engine-api v0.4.0 github.com/dustin/go-humanize v1.0.0 @@ -18,6 +19,7 @@ require ( github.com/go-micro/plugins/v4/broker/stan v1.1.0 github.com/go-micro/plugins/v4/registry/etcd v1.1.0 github.com/go-playground/validator/v10 v10.19.0 + github.com/go-redis/redis/v8 v8.11.5 github.com/go-resty/resty/v2 v2.12.0 github.com/go-sql-driver/mysql v1.7.1 github.com/golang-jwt/jwt/v4 v4.5.0 @@ -74,6 +76,7 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/TencentBlueKing/gopkg v1.1.0 // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect @@ -86,6 +89,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect @@ -114,6 +118,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/gomodule/redigo v1.9.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect @@ -165,6 +170,7 @@ require ( github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect go.etcd.io/etcd/api/v3 v3.5.2 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 // indirect