mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
05823cd74f
* 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>
390 lines
10 KiB
Go
390 lines
10 KiB
Go
package version_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"testing"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
guuid "github.com/gofrs/uuid"
|
|
goredis "github.com/redis/go-redis/v9"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"go.etcd.io/bbolt"
|
|
|
|
"zotregistry.dev/zot/pkg/log"
|
|
"zotregistry.dev/zot/pkg/meta/boltdb"
|
|
mdynamodb "zotregistry.dev/zot/pkg/meta/dynamodb"
|
|
"zotregistry.dev/zot/pkg/meta/redis"
|
|
"zotregistry.dev/zot/pkg/meta/version"
|
|
tskip "zotregistry.dev/zot/pkg/test/skip"
|
|
)
|
|
|
|
var ErrTestError = errors.New("test error")
|
|
|
|
func TestVersioningBoltDB(t *testing.T) {
|
|
Convey("Tests", t, func() {
|
|
tmpDir := t.TempDir()
|
|
boltDBParams := boltdb.DBParameters{RootDir: tmpDir}
|
|
boltDriver, err := boltdb.GetBoltDriver(boltDBParams)
|
|
So(err, ShouldBeNil)
|
|
|
|
log := log.NewLogger("debug", "")
|
|
|
|
boltdbWrapper, err := boltdb.New(boltDriver, log)
|
|
|
|
defer os.Remove(path.Join(boltDBParams.RootDir, "meta.db"))
|
|
So(boltdbWrapper, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
|
|
boltdbWrapper.Patches = []func(DB *bbolt.DB) error{
|
|
func(DB *bbolt.DB) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
Convey("success", func() {
|
|
boltdbWrapper.Patches = []func(DB *bbolt.DB) error{
|
|
func(DB *bbolt.DB) error { // V1 to V2
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := setBoltDBVersion(boltdbWrapper.DB, version.Version1)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = boltdbWrapper.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("DBVersion is empty", func() {
|
|
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
|
versionBuck := tx.Bucket([]byte(boltdb.VersionBucket))
|
|
|
|
return versionBuck.Put([]byte(version.DBVersionKey), []byte(""))
|
|
})
|
|
So(err, ShouldBeNil)
|
|
|
|
err = boltdbWrapper.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("iterate patches with skip", func() {
|
|
boltdbWrapper.Patches = []func(DB *bbolt.DB) error{
|
|
func(DB *bbolt.DB) error { // V1 to V2
|
|
return nil
|
|
},
|
|
func(DB *bbolt.DB) error { // V2 to V3
|
|
return nil
|
|
},
|
|
func(DB *bbolt.DB) error { // V3 to V4
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := setBoltDBVersion(boltdbWrapper.DB, version.Version1)
|
|
So(err, ShouldBeNil)
|
|
// we should skip the first patch
|
|
|
|
err = boltdbWrapper.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("patch has error", func() {
|
|
boltdbWrapper.Patches = []func(DB *bbolt.DB) error{
|
|
func(DB *bbolt.DB) error { // V1 to V2
|
|
return ErrTestError
|
|
},
|
|
}
|
|
|
|
err = boltdbWrapper.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func setBoltDBVersion(db *bbolt.DB, vers string) error {
|
|
err := db.Update(func(tx *bbolt.Tx) error {
|
|
versionBuck := tx.Bucket([]byte(boltdb.VersionBucket))
|
|
|
|
return versionBuck.Put([]byte(version.DBVersionKey), []byte(vers))
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func TestVersioningDynamoDB(t *testing.T) {
|
|
tskip.SkipDynamo(t)
|
|
|
|
uuid, err := guuid.NewV4()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
Convey("Tests", t, func() {
|
|
params := mdynamodb.DBDriverParameters{
|
|
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
|
|
Region: "us-east-2",
|
|
RepoMetaTablename: "RepoMetadataTable" + uuid.String(),
|
|
RepoBlobsInfoTablename: "RepoBlobsInfoTablename" + uuid.String(),
|
|
ImageMetaTablename: "ImageMetaTablename" + uuid.String(),
|
|
UserDataTablename: "UserDataTable" + uuid.String(),
|
|
APIKeyTablename: "ApiKeyTable" + uuid.String(),
|
|
VersionTablename: "Version" + uuid.String(),
|
|
}
|
|
|
|
dynamoClient, err := mdynamodb.GetDynamoClient(params)
|
|
So(err, ShouldBeNil)
|
|
|
|
log := log.NewLogger("debug", "")
|
|
|
|
dynamoWrapper, err := mdynamodb.New(dynamoClient, params, log)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil)
|
|
|
|
Convey("dbVersion is empty", func() {
|
|
err := setDynamoDBVersion(dynamoWrapper.Client, params.VersionTablename, "")
|
|
So(err, ShouldBeNil)
|
|
|
|
err = dynamoWrapper.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("iterate patches with skip", func() {
|
|
dynamoWrapper.Patches = []func(client *dynamodb.Client, tableNames map[string]string) error{
|
|
func(client *dynamodb.Client, tableNames map[string]string) error { // V1 to V2
|
|
return nil
|
|
},
|
|
func(client *dynamodb.Client, tableNames map[string]string) error { // V2 to V3
|
|
return nil
|
|
},
|
|
func(client *dynamodb.Client, tableNames map[string]string) error { // V3 to V4
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := setDynamoDBVersion(dynamoWrapper.Client, params.VersionTablename, version.Version1)
|
|
So(err, ShouldBeNil)
|
|
// we should skip the first patch
|
|
|
|
err = dynamoWrapper.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("patch has error", func() {
|
|
dynamoWrapper.Patches = []func(client *dynamodb.Client, tableNames map[string]string) error{
|
|
func(client *dynamodb.Client, tableNames map[string]string) error { // V1 to V2
|
|
return ErrTestError
|
|
},
|
|
}
|
|
|
|
err = dynamoWrapper.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func setDynamoDBVersion(client *dynamodb.Client, versTable, vers string) error {
|
|
mdAttributeValue, err := attributevalue.Marshal(vers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
|
|
ExpressionAttributeNames: map[string]string{
|
|
"#V": "Version",
|
|
},
|
|
ExpressionAttributeValues: map[string]types.AttributeValue{
|
|
":Version": mdAttributeValue,
|
|
},
|
|
Key: map[string]types.AttributeValue{
|
|
"TableKey": &types.AttributeValueMemberS{
|
|
Value: version.DBVersionKey,
|
|
},
|
|
},
|
|
TableName: aws.String(versTable),
|
|
UpdateExpression: aws.String("SET #V = :Version"),
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func TestVersioningRedisDB(t *testing.T) {
|
|
miniRedis := miniredis.RunT(t)
|
|
|
|
Convey("Tests", t, func() {
|
|
opts, err := goredis.ParseURL("redis://" + miniRedis.Addr())
|
|
So(err, ShouldBeNil)
|
|
|
|
client := goredis.NewClient(opts)
|
|
defer dumpRedisKeys(t, client) // Troubleshoot test failures
|
|
|
|
log := log.NewLogger("debug", "")
|
|
|
|
params := redis.DBDriverParameters{KeyPrefix: "zot"}
|
|
|
|
metaDB, err := redis.New(client, params, log)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(metaDB.ResetDB(), ShouldBeNil)
|
|
|
|
ctx := context.Background()
|
|
|
|
Convey("empty initial version triggers setting the default", func() {
|
|
// Check no value is initially set
|
|
actualVersion, err := client.Get(ctx, metaDB.VersionKey).Result()
|
|
So(err, ShouldEqual, goredis.Nil)
|
|
So(actualVersion, ShouldEqual, "")
|
|
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
|
|
// Check default version is added in the DB
|
|
actualVersion, err = client.Get(ctx, metaDB.VersionKey).Result()
|
|
So(err, ShouldBeNil)
|
|
So(actualVersion, ShouldEqual, version.CurrentVersion)
|
|
})
|
|
|
|
Convey("initial version with a bad value raises an error", func() {
|
|
// Set invalid initial value
|
|
err = client.Set(ctx, metaDB.VersionKey, "VInvalid", 0).Err()
|
|
So(err, ShouldBeNil)
|
|
|
|
// Check error when attempting to patch
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("skip iterating patches", func() {
|
|
// Initialize DB version
|
|
metaDB.Version = version.Version1
|
|
|
|
// Patches have errors so we can check bad upgrade logic
|
|
metaDB.Patches = []func(client goredis.UniversalClient) error{
|
|
func(client goredis.UniversalClient) error { // V1 to V2
|
|
return ErrTestError
|
|
},
|
|
func(client goredis.UniversalClient) error { // V2 to V3
|
|
return ErrTestError
|
|
},
|
|
}
|
|
|
|
// No patch should be applied for V1 so no error is expected
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("iterate over patches without any errors", func() {
|
|
// Initialize DB version with a lower version
|
|
metaDB.Version = version.Version1
|
|
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
|
|
// Now change to a newer DB version and apply patches
|
|
metaDB.Version = version.Version3
|
|
|
|
metaDB.Patches = []func(goredis.UniversalClient) error{
|
|
func(client goredis.UniversalClient) error { // V1 to V2
|
|
return nil
|
|
},
|
|
func(client goredis.UniversalClient) error { // V2 to V3
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("iterate over patches with errors", func() {
|
|
// initialize DB version with a lower version
|
|
metaDB.Version = version.Version1
|
|
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldBeNil)
|
|
|
|
// now change to a newer DB version and apply patches
|
|
metaDB.Version = version.Version3
|
|
|
|
metaDB.Patches = []func(client goredis.UniversalClient) error{
|
|
func(client goredis.UniversalClient) error { // V1 to V2
|
|
return nil
|
|
},
|
|
func(client goredis.UniversalClient) error { // V2 to V3
|
|
return ErrTestError
|
|
},
|
|
}
|
|
|
|
err = metaDB.PatchDB()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func dumpRedisKeys(t *testing.T, client goredis.UniversalClient) {
|
|
t.Helper()
|
|
|
|
// Retrieve all keys
|
|
keys, err := client.Keys(context.Background(), "*").Result()
|
|
if err != nil {
|
|
t.Log("Error retrieving keys:", err)
|
|
|
|
return
|
|
}
|
|
|
|
// Print the keys
|
|
t.Log("Keys in Redis:")
|
|
|
|
for _, key := range keys {
|
|
keyType, err := client.Type(context.Background(), key).Result()
|
|
if err != nil {
|
|
t.Logf("Error retrieving type for key %s: %v\n", key, err)
|
|
|
|
continue
|
|
}
|
|
|
|
var value string
|
|
|
|
switch keyType {
|
|
case "string":
|
|
value, err = client.Get(context.Background(), key).Result()
|
|
case "list":
|
|
values, err := client.LRange(context.Background(), key, 0, -1).Result()
|
|
if err == nil {
|
|
value = fmt.Sprintf("%v", values)
|
|
}
|
|
case "hash":
|
|
values, err := client.HGetAll(context.Background(), key).Result()
|
|
if err == nil {
|
|
value = fmt.Sprintf("%v", values)
|
|
}
|
|
case "set":
|
|
values, err := client.SMembers(context.Background(), key).Result()
|
|
if err == nil {
|
|
value = fmt.Sprintf("%v", values)
|
|
}
|
|
case "zset":
|
|
values, err := client.ZRange(context.Background(), key, 0, -1).Result()
|
|
if err == nil {
|
|
value = fmt.Sprintf("%v", values)
|
|
}
|
|
default:
|
|
value = "Unsupported type"
|
|
}
|
|
|
|
if err != nil {
|
|
t.Logf("Error retrieving value for key %s: %v\n", key, err)
|
|
} else {
|
|
t.Logf("Key: %s, Type: %s, Value: %s\n", key, keyType, value)
|
|
}
|
|
}
|
|
}
|