feat(cache): dynamodb implementation (#953)

Signed-off-by: Catalin Hofnar <catalin.hofnar@gmail.com>
This commit is contained in:
Catalin-George Hofnar
2022-11-22 20:29:57 +02:00
committed by GitHub
parent 49c3d05706
commit 31b9481713
22 changed files with 1746 additions and 48 deletions
+4
View File
@@ -12,6 +12,10 @@ func Create(dbtype string, parameters interface{}, log zlog.Logger) (cache.Cache
{
return cache.NewBoltDBCache(parameters, log), nil
}
case "dynamodb":
{
return cache.NewDynamoDBCache(parameters, log), nil
}
default:
{
return nil, errors.ErrBadConfig
+9 -8
View File
@@ -32,18 +32,14 @@ func NewBoltDBCache(parameters interface{}, log zlog.Logger) Cache {
panic("Failed type assertion")
}
return NewCache(properParameters, log)
}
func NewCache(parameters BoltDBDriverParameters, log zlog.Logger) *BoltDBDriver {
err := os.MkdirAll(parameters.RootDir, constants.DefaultDirPerms)
err := os.MkdirAll(properParameters.RootDir, constants.DefaultDirPerms)
if err != nil {
log.Error().Err(err).Msgf("unable to create directory for cache db: %v", parameters.RootDir)
log.Error().Err(err).Msgf("unable to create directory for cache db: %v", properParameters.RootDir)
return nil
}
dbPath := path.Join(parameters.RootDir, parameters.Name+constants.DBExtensionName)
dbPath := path.Join(properParameters.RootDir, properParameters.Name+constants.DBExtensionName)
dbOpts := &bbolt.Options{
Timeout: constants.DBCacheLockCheckTimeout,
FreelistType: bbolt.FreelistArrayType,
@@ -72,7 +68,12 @@ func NewCache(parameters BoltDBDriverParameters, log zlog.Logger) *BoltDBDriver
return nil
}
return &BoltDBDriver{rootDir: parameters.RootDir, db: cacheDB, useRelPaths: parameters.UseRelPaths, log: log}
return &BoltDBDriver{
rootDir: properParameters.RootDir,
db: cacheDB,
useRelPaths: properParameters.UseRelPaths,
log: log,
}
}
func (d *BoltDBDriver) Name() string {
+216
View File
@@ -0,0 +1,216 @@
package cache
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"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"
godigest "github.com/opencontainers/go-digest"
zerr "zotregistry.io/zot/errors"
zlog "zotregistry.io/zot/pkg/log"
)
type DynamoDBDriver struct {
client *dynamodb.Client
log zlog.Logger
tableName string
}
type DynamoDBDriverParameters struct {
Endpoint, Region, TableName string
}
type Blob struct {
Digest string `dynamodbav:"Digest,string"`
BlobPath []string `dynamodbav:"BlobPath,stringset"`
}
// Use ONLY for tests.
func (d *DynamoDBDriver) NewTable(tableName string) error {
//nolint:gomnd
_, err := d.client.CreateTable(context.TODO(), &dynamodb.CreateTableInput{
TableName: &tableName,
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("Digest"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("Digest"),
KeyType: types.KeyTypeHash,
},
},
ProvisionedThroughput: &types.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(10),
WriteCapacityUnits: aws.Int64(5),
},
})
if err != nil {
return err
}
d.tableName = tableName
return nil
}
func NewDynamoDBCache(parameters interface{}, log zlog.Logger) Cache {
properParameters, ok := parameters.(DynamoDBDriverParameters)
if !ok {
panic("Failed type assertion!")
}
// custom endpoint resolver to point to localhost
customResolver := aws.EndpointResolverWithOptionsFunc(
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
PartitionID: "aws",
URL: properParameters.Endpoint,
SigningRegion: region,
}, nil
})
// Using the SDK's default configuration, loading additional config
// and credentials values from the environment variables, shared
// credentials, and shared configuration files
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(properParameters.Region),
config.WithEndpointResolverWithOptions(customResolver))
if err != nil {
log.Error().Msgf("unable to load AWS SDK config for dynamodb, %v", err)
return nil
}
// Using the Config value, create the DynamoDB client
return &DynamoDBDriver{client: dynamodb.NewFromConfig(cfg), tableName: properParameters.TableName, log: log}
}
func (d *DynamoDBDriver) Name() string {
return "dynamodb"
}
// Returns the first path of the blob if it exists.
func (d *DynamoDBDriver) GetBlob(digest godigest.Digest) (string, error) {
resp, err := d.client.GetItem(context.TODO(), &dynamodb.GetItemInput{
TableName: aws.String(d.tableName),
Key: map[string]types.AttributeValue{
"Digest": &types.AttributeValueMemberS{Value: digest.String()},
},
})
if err != nil {
d.log.Error().Msgf("failed to get blob %v, %v", d.tableName, err)
return "", err
}
out := Blob{}
if resp.Item == nil {
return "", zerr.ErrCacheMiss
}
_ = attributevalue.UnmarshalMap(resp.Item, &out)
if len(out.BlobPath) == 0 {
return "", nil
}
return out.BlobPath[0], nil
}
func (d *DynamoDBDriver) PutBlob(digest godigest.Digest, path string) error {
if path == "" {
d.log.Error().Err(zerr.ErrEmptyValue).Str("digest", digest.String()).Msg("empty path provided")
return zerr.ErrEmptyValue
}
marshaledKey, _ := attributevalue.MarshalMap(map[string]interface{}{"Digest": digest.String()})
expression := "ADD BlobPath :i"
attrPath := types.AttributeValueMemberSS{Value: []string{path}}
if _, err := d.client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
Key: marshaledKey,
TableName: &d.tableName,
UpdateExpression: &expression,
ExpressionAttributeValues: map[string]types.AttributeValue{":i": &attrPath},
}); err != nil {
d.log.Error().Err(err)
return err
}
return nil
}
func (d *DynamoDBDriver) HasBlob(digest godigest.Digest, path string) bool {
resp, err := d.client.GetItem(context.TODO(), &dynamodb.GetItemInput{
TableName: aws.String(d.tableName),
Key: map[string]types.AttributeValue{
"Digest": &types.AttributeValueMemberS{Value: digest.String()},
},
})
if err != nil {
d.log.Error().Msgf("failed to get blob %v, %v", d.tableName, err)
return false
}
out := Blob{}
if resp.Item == nil {
d.log.Error().Err(zerr.ErrCacheMiss)
return false
}
_ = attributevalue.UnmarshalMap(resp.Item, &out)
for _, item := range out.BlobPath {
if item == path {
return true
}
}
d.log.Error().Err(zerr.ErrCacheMiss)
return false
}
func (d *DynamoDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
marshaledKey, _ := attributevalue.MarshalMap(map[string]interface{}{"Digest": digest.String()})
expression := "DELETE BlobPath :i"
attrPath := types.AttributeValueMemberSS{Value: []string{path}}
_, err := d.client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
Key: marshaledKey,
TableName: &d.tableName,
UpdateExpression: &expression,
ExpressionAttributeValues: map[string]types.AttributeValue{":i": &attrPath},
})
if err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("path", path).Msg("unable to delete")
return err
}
result, _ := d.GetBlob(digest)
if result == "" {
d.log.Debug().Str("digest", digest.String()).Str("path", path).Msg("deleting empty bucket")
_, _ = d.client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{
Key: marshaledKey,
TableName: &d.tableName,
})
}
return nil
}
+111
View File
@@ -0,0 +1,111 @@
package cache_test
import (
"os"
"path"
"testing"
godigest "github.com/opencontainers/go-digest"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/cache"
)
func skipIt(t *testing.T) {
t.Helper()
if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" {
t.Skip("Skipping testing without AWS DynamoDB mock server")
}
}
func TestDynamoDB(t *testing.T) {
skipIt(t)
Convey("Test dynamoDB", t, func(c C) {
log := log.NewLogger("debug", "")
dir := t.TempDir()
// bad params
So(func() {
_ = cache.NewDynamoDBCache("bad params", log)
}, ShouldPanic)
keyDigest := godigest.FromString("key")
cacheDriver, err := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: "http://brokenlink",
TableName: "BlobTable",
Region: "us-east-2",
}, log)
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
val, err := cacheDriver.GetBlob(keyDigest)
So(err, ShouldNotBeNil)
So(val, ShouldBeEmpty)
err = cacheDriver.PutBlob(keyDigest, path.Join(dir, "value"))
So(err, ShouldNotBeNil)
exists := cacheDriver.HasBlob(keyDigest, path.Join(dir, "value"))
So(exists, ShouldBeFalse)
err = cacheDriver.DeleteBlob(keyDigest, path.Join(dir, "value"))
So(err, ShouldNotBeNil)
cacheDriver, err = storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
TableName: "BlobTable",
Region: "us-east-2",
}, log)
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
returnedName := cacheDriver.Name()
So(returnedName, ShouldEqual, "dynamodb")
val, err = cacheDriver.GetBlob(keyDigest)
So(err, ShouldNotBeNil)
So(val, ShouldBeEmpty)
err = cacheDriver.PutBlob(keyDigest, "")
So(err, ShouldNotBeNil)
err = cacheDriver.PutBlob(keyDigest, path.Join(dir, "value"))
So(err, ShouldBeNil)
val, err = cacheDriver.GetBlob(keyDigest)
So(err, ShouldBeNil)
So(val, ShouldNotBeEmpty)
exists = cacheDriver.HasBlob(keyDigest, path.Join(dir, "value"))
So(exists, ShouldBeTrue)
err = cacheDriver.DeleteBlob(keyDigest, path.Join(dir, "value"))
So(err, ShouldBeNil)
exists = cacheDriver.HasBlob(keyDigest, path.Join(dir, "value"))
So(exists, ShouldBeFalse)
err = cacheDriver.PutBlob(keyDigest, path.Join(dir, "value1"))
So(err, ShouldBeNil)
err = cacheDriver.PutBlob(keyDigest, path.Join(dir, "value2"))
So(err, ShouldBeNil)
err = cacheDriver.DeleteBlob(keyDigest, path.Join(dir, "value1"))
So(err, ShouldBeNil)
exists = cacheDriver.HasBlob(keyDigest, path.Join(dir, "value2"))
So(exists, ShouldBeTrue)
exists = cacheDriver.HasBlob(keyDigest, path.Join(dir, "value1"))
So(exists, ShouldBeFalse)
err = cacheDriver.DeleteBlob(keyDigest, path.Join(dir, "value2"))
So(err, ShouldBeNil)
})
}
+520
View File
@@ -0,0 +1,520 @@
package storage_test
import (
"math/rand"
"os/exec"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/cache"
)
const (
region string = "us-east-2"
localEndpoint string = "http://localhost:4566"
awsEndpoint string = "https://dynamodb.us-east-2.amazonaws.com"
datasetSize int = 5000
)
func generateRandomString() string {
//nolint: gosec
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
charset := "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
randomBytes := make([]byte, 10)
for i := range randomBytes {
randomBytes[i] = charset[seededRand.Intn(len(charset))]
}
return string(randomBytes)
}
func generateData() map[godigest.Digest]string {
dataMap := make(map[godigest.Digest]string, datasetSize)
//nolint: gosec
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < datasetSize; i++ {
randomString := generateRandomString()
counter := 0
for seededRand.Float32() < 0.5 && counter < 5 {
counter++
randomString += "/"
randomString += generateRandomString()
}
digest := godigest.FromString(randomString)
dataMap[digest] = randomString
}
return dataMap
}
func helperPutAll(cache cache.Cache, testData map[godigest.Digest]string) {
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
}
func helperDeleteAll(cache cache.Cache, testData map[godigest.Digest]string) {
for digest, path := range testData {
_ = cache.DeleteBlob(digest, path)
}
}
func helperHasAll(cache cache.Cache, testData map[godigest.Digest]string) {
for digest, path := range testData {
_ = cache.HasBlob(digest, path)
}
}
func helperGetAll(cache cache.Cache, testData map[godigest.Digest]string) {
for digest := range testData {
_, _ = cache.GetBlob(digest)
}
}
func helperMix(cache cache.Cache, testData map[godigest.Digest]string, digestSlice []godigest.Digest) {
// The test data contains datasetSize entries by default, and each set of operations uses 5 entries
for i := 0; i < 1000; i++ {
_ = cache.PutBlob(digestSlice[i*5], testData[digestSlice[i*5]])
_ = cache.PutBlob(digestSlice[i*5+1], testData[digestSlice[i*5+1]])
_ = cache.PutBlob(digestSlice[i*5+2], testData[digestSlice[i*5+2]])
_ = cache.PutBlob(digestSlice[i*5+2], testData[digestSlice[i*5+3]])
_ = cache.DeleteBlob(digestSlice[i*5+1], testData[digestSlice[i*5+1]])
_ = cache.DeleteBlob(digestSlice[i*5+2], testData[digestSlice[i*5+3]])
_ = cache.DeleteBlob(digestSlice[i*5+2], testData[digestSlice[i*5+2]])
_ = cache.HasBlob(digestSlice[i*5], testData[digestSlice[i*5]])
_ = cache.HasBlob(digestSlice[i*5+1], testData[digestSlice[i*5+1]])
_, _ = cache.GetBlob(digestSlice[i*5])
_, _ = cache.GetBlob(digestSlice[i*5+1])
_ = cache.PutBlob(digestSlice[i*5], testData[digestSlice[i*5+4]])
_, _ = cache.GetBlob(digestSlice[i*5+4])
_ = cache.DeleteBlob(digestSlice[i*5], testData[digestSlice[i*5+4]])
_ = cache.DeleteBlob(digestSlice[i*5], testData[digestSlice[i*5]])
}
}
// BoltDB tests
func BenchmarkPutLocal(b *testing.B) {
dir := b.TempDir()
log := log.NewLogger("error", "")
cache, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache_test",
UseRelPaths: false,
}, log)
testData := generateData()
b.ResetTimer()
helperPutAll(cache, testData)
}
func BenchmarkDeleteLocal(b *testing.B) {
dir := b.TempDir()
log := log.NewLogger("error", "")
cache, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache_test",
UseRelPaths: false,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperDeleteAll(cache, testData)
}
func BenchmarkHasLocal(b *testing.B) {
dir := b.TempDir()
log := log.NewLogger("error", "")
cache, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache_test",
UseRelPaths: false,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperHasAll(cache, testData)
}
func BenchmarkGetLocal(b *testing.B) {
dir := b.TempDir()
log := log.NewLogger("error", "")
cache, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache_test",
UseRelPaths: false,
}, log)
testData := generateData()
counter := 1
var previousDigest godigest.Digest
for digest, path := range testData {
if counter != 10 {
_ = cache.PutBlob(digest, path)
previousDigest = digest
counter++
} else {
_ = cache.PutBlob(previousDigest, path)
counter = 1
}
}
b.ResetTimer()
helperGetAll(cache, testData)
}
func BenchmarkMixLocal(b *testing.B) {
dir := b.TempDir()
log := log.NewLogger("error", "")
cache, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache_test",
UseRelPaths: false,
}, log)
testData := generateData()
digestSlice := make([]godigest.Digest, datasetSize)
counter := 0
for key := range testData {
digestSlice[counter] = key
counter++
}
b.ResetTimer()
helperMix(cache, testData, digestSlice)
}
// DynamoDB Local tests
func BenchmarkPutLocalstack(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", localEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: localEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
b.ResetTimer()
helperPutAll(cache, testData)
}
func BenchmarkDeleteLocalstack(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", localEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: localEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperDeleteAll(cache, testData)
}
func BenchmarkHasLocalstack(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", localEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: localEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperHasAll(cache, testData)
}
func BenchmarkGetLocalstack(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", localEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: localEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
counter := 1
var previousDigest godigest.Digest
for digest, path := range testData {
if counter != 10 {
_ = cache.PutBlob(digest, path)
previousDigest = digest
counter++
} else {
_ = cache.PutBlob(previousDigest, path)
counter = 1
}
}
b.ResetTimer()
helperGetAll(cache, testData)
}
func BenchmarkMixLocalstack(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", localEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: localEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
digestSlice := make([]godigest.Digest, datasetSize)
counter := 0
for key := range testData {
digestSlice[counter] = key
counter++
}
b.ResetTimer()
helperMix(cache, testData, digestSlice)
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// DANGER ZONE: tests with true AWS endpoint
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
func BenchmarkPutAWS(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", awsEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: awsEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
b.ResetTimer()
helperPutAll(cache, testData)
}
func BenchmarkDeleteAWS(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", awsEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: awsEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperDeleteAll(cache, testData)
}
func BenchmarkHasAWS(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", awsEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: awsEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
for digest, path := range testData {
_ = cache.PutBlob(digest, path)
}
b.ResetTimer()
helperHasAll(cache, testData)
}
func BenchmarkGetAWS(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", awsEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: awsEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
counter := 1
var previousDigest godigest.Digest
for digest, path := range testData {
if counter != 10 {
_ = cache.PutBlob(digest, path)
previousDigest = digest
counter++
} else {
_ = cache.PutBlob(previousDigest, path)
counter = 1
}
}
b.ResetTimer()
helperGetAll(cache, testData)
}
func BenchmarkMixAWS(b *testing.B) {
log := log.NewLogger("error", "")
tableName := generateRandomString()
// Create Table
_, err := exec.Command("aws", "dynamodb", "--region", region, "--endpoint-url", awsEndpoint, "create-table",
"--table-name", tableName, "--attribute-definitions", "AttributeName=Digest,AttributeType=S",
"--key-schema", "AttributeName=Digest,KeyType=HASH",
"--billing-mode", "PAY_PER_REQUEST").Output()
if err != nil {
panic(err)
}
cache, _ := storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: awsEndpoint,
Region: region,
TableName: tableName,
}, log)
testData := generateData()
digestSlice := make([]godigest.Digest, datasetSize)
counter := 0
for key := range testData {
digestSlice[counter] = key
counter++
}
b.ResetTimer()
helperMix(cache, testData, digestSlice)
}
+1 -1
View File
@@ -19,5 +19,5 @@ const (
DBCacheLockCheckTimeout = 10 * time.Second
BoltdbName = "cache"
ReferrerFilterAnnotation = "org.opencontainers.references.filtersApplied"
//
DynamoDBDriverName = "dynamodb"
)
+250 -2
View File
@@ -44,6 +44,7 @@ var (
fileInfoSize = 10
errorText = "new s3 error"
errS3 = errors.New(errorText)
zotStorageTest = "zot-storage-test"
)
func cleanupStorage(store driver.StorageDriver, name string) {
@@ -56,6 +57,10 @@ func skipIt(t *testing.T) {
if os.Getenv("S3MOCK_ENDPOINT") == "" {
t.Skip("Skipping testing without AWS S3 mock server")
}
if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" {
t.Skip("Skipping testing without AWS DynamoDB mock server")
}
}
func createMockStorage(rootDir string, cacheDir string, dedupe bool, store driver.StorageDriver) storage.ImageStore {
@@ -84,7 +89,7 @@ func createObjectsStore(rootDir string, cacheDir string, dedupe bool) (
storage.ImageStore,
error,
) {
bucket := "zot-storage-test"
bucket := zotStorageTest
endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{
"rootDir": rootDir,
@@ -130,6 +135,66 @@ func createObjectsStore(rootDir string, cacheDir string, dedupe bool) (
return store, il, err
}
func createObjectsStoreDynamo(rootDir string, cacheDir string, dedupe bool, tableName string) (
driver.StorageDriver,
storage.ImageStore,
error,
) {
bucket := zotStorageTest
endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{
"rootDir": rootDir,
"name": "s3",
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false,
"skipverify": false,
}
storeName := fmt.Sprintf("%v", storageDriverParams["name"])
store, err := factory.Create(storeName, storageDriverParams)
if err != nil {
panic(err)
}
// create bucket if it doesn't exists
_, err = resty.R().Put("http://" + endpoint + "/" + bucket)
if err != nil {
panic(err)
}
log := log.Logger{Logger: zerolog.New(os.Stdout)}
metrics := monitoring.NewMetricsServer(false, log)
var cacheDriver cache.Cache
// from pkg/cli/root.go/applyDefaultValues, s3 magic
cacheDriver, _ = storage.Create("dynamodb", cache.DynamoDBDriverParameters{
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
Region: os.Getenv("us-east-2"),
TableName: tableName,
}, log)
tableName = strings.ReplaceAll(tableName, "/", "")
//nolint:errcheck
cacheDriverDynamo, _ := cacheDriver.(*cache.DynamoDBDriver)
err = cacheDriverDynamo.NewTable(tableName)
if err != nil {
panic(err)
}
il := s3.NewImageStore(rootDir, cacheDir, false, storage.DefaultGCDelay,
dedupe, false, log, metrics, nil, store, cacheDriver)
return store, il, err
}
type FileInfoMock struct {
IsDirFn func() bool
SizeFn func() int64
@@ -607,7 +672,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
})
Convey("Unable to create subpath cache db", func(c C) {
bucket := "zot-storage-test"
bucket := zotStorageTest
endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := config.GlobalStorageConfig{
@@ -1448,6 +1513,189 @@ func TestS3Dedupe(t *testing.T) {
So(fi1.Size(), ShouldEqual, fi3.Size())
})
})
Convey("Dedupe with dynamodb", t, func(c C) {
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, _ := createObjectsStoreDynamo(testDir, tdir, true, tdir)
defer cleanupStorage(storeDriver, testDir)
// manifest1
upload, err := imgStore.NewBlobUpload("dedupe1")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data3")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed("dedupe1", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest1 := digest
So(blobDigest1, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe1", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
_, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest)
So(checkBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
blobReadCloser, getBlobSize1, err := imgStore.GetBlob("dedupe1", digest,
"application/vnd.oci.image.layer.v1.tar+gzip")
So(getBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
err = blobReadCloser.Close()
So(err, ShouldBeNil)
cblob, cdigest := test.GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("dedupe1", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("dedupe1", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, err = imgStore.PutImageManifest("dedupe1", digest.String(),
ispec.MediaTypeImageManifest, manifestBuf)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe1", digest.String())
So(err, ShouldBeNil)
// manifest2
upload, err = imgStore.NewBlobUpload("dedupe2")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data3")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
digest = godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunkStreamed("dedupe2", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest2 := digest
So(blobDigest2, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe2", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
_, checkBlobSize2, err := imgStore.CheckBlob("dedupe2", digest)
So(err, ShouldBeNil)
So(checkBlobSize2, ShouldBeGreaterThan, 0)
blobReadCloser, getBlobSize2, err := imgStore.GetBlob("dedupe2", digest,
"application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
So(getBlobSize2, ShouldBeGreaterThan, 0)
So(checkBlobSize1, ShouldEqual, checkBlobSize2)
So(getBlobSize1, ShouldEqual, getBlobSize2)
err = blobReadCloser.Close()
So(err, ShouldBeNil)
cblob, cdigest = test.GetRandomImageConfig()
_, clen, err = imgStore.FullBlobUpload("dedupe2", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err = imgStore.CheckBlob("dedupe2", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest = ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest,
manifestBuf)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe2", digest.String())
So(err, ShouldBeNil)
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldBeNil)
fi2, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
// original blob should have the real content of blob
So(fi1.Size(), ShouldNotEqual, fi2.Size())
So(fi1.Size(), ShouldBeGreaterThan, 0)
// deduped blob should be of size 0
So(fi2.Size(), ShouldEqual, 0)
Convey("Check that delete blobs moves the real content to the next contenders", func() {
// if we delete blob1, the content should be moved to blob2
err = imgStore.DeleteBlob("dedupe1", blobDigest1)
So(err, ShouldBeNil)
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldNotBeNil)
fi2, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
So(fi2.Size(), ShouldBeGreaterThan, 0)
// the second blob should now be equal to the deleted blob.
So(fi2.Size(), ShouldEqual, fi1.Size())
err = imgStore.DeleteBlob("dedupe2", blobDigest2)
So(err, ShouldBeNil)
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldNotBeNil)
})
})
}
func TestS3PullRange(t *testing.T) {