Skip to content

Commit 02e8ebe

Browse files
feat(storage): support DynamoDB tablenamePrefix for cache driver
Add a tablenamePrefix option to the dynamodb cacheDriver so administrators can configure all DynamoDB table names with a single prefix instead of declaring each table individually. Explicit per-table names continue to override derived names for backward compatibility. Fixes #2966 Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent 8282aef commit 02e8ebe

7 files changed

Lines changed: 350 additions & 32 deletions

File tree

examples/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,15 +1004,18 @@ To set up a zot with dedupe enabled and dynamodb as a cache driver, "cacheDriver
10041004
"cacheDriver": {
10051005
"name": "dynamodb", // driver name
10061006
"endpoint": "http://localhost:4566", // aws endpoint
1007-
"region": "us-east-2" // aws region
1008-
"cacheTablename": "ZotBlobTable" // table used to store deduped blobs
1009-
1007+
"region": "us-east-2", // aws region
1008+
"tablenamePrefix": "Zot" // table name prefix
10101009
}
10111010
},
10121011
```
10131012
Like s3 configuration AWS GO SDK will load additional config and credentials values from the environment variables, shared credentials, and shared configuration files
10141013

1015-
Additionally if search extension is enabled, additional parameters are needed:
1014+
With the `tablenamePrefix` value above, zot uses `ZotBlobTable` for dedupe cache data. When auth or the search
1015+
extension is enabled, zot also uses `ZotUserDataTable`, `ZotApiKeyDataTable`, `ZotRepoMetadataTable`,
1016+
`ZotImageMetaTable`, `ZotRepoBlobsInfoTable`, and `ZotVersionTable`.
1017+
1018+
Individual table names can still be configured for backward compatibility or to override a derived name:
10161019

10171020
```
10181021
"cacheDriver": {
@@ -1027,12 +1030,13 @@ Additionally if search extension is enabled, additional parameters are needed:
10271030
"repoMetaTablename": "ZotRepoMetadataTable",
10281031
"imageMetaTablename": "ZotImageMetaTable",
10291032
"repoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
1030-
"versionTablename": "ZotVersion"
1033+
"versionTablename": "ZotVersionTable"
10311034
}
10321035
```
10331036

10341037
### DynamoDB permission scopes
1035-
The following AWS policy is required by zot for caching blobs. Make sure to replace DYNAMODB_TABLE with the name of your table which in our case is the value of "cacheTablename" (ZotBlobTable)
1038+
The following AWS policy is required by zot for caching blobs. Make sure to replace DYNAMODB_TABLE with the name of
1039+
your table. When `tablenamePrefix` is used, this includes the generated table names described above.
10361040

10371041
{
10381042
"Version": "2012-10-17",

examples/config-dynamodb.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@
1818
"name": "dynamodb",
1919
"endpoint": "http://localhost:4566",
2020
"region": "us-east-2",
21-
"cacheTablename": "ZotBlobTable",
22-
"repoMetaTablename": "ZotRepoMetadataTable",
23-
"imageMetaTablename": "ZotImageMetaTable",
24-
"repoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
25-
"userDataTablename": "ZotUserDataTable",
26-
"apiKeyTablename": "ZotApiKeyTable",
27-
"versionTablename": "ZotVersion"
21+
"tablenamePrefix": "Zot"
2822
}
2923
},
3024
"http": {

pkg/meta/meta.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package meta
22

33
import (
44
"fmt"
5+
"strings"
56

67
"zotregistry.dev/zot/v2/errors"
78
"zotregistry.dev/zot/v2/pkg/api/config"
@@ -69,22 +70,32 @@ func getDynamoParams(cacheDriverConfig map[string]any, log log.Logger) mdynamodb
6970
region, ok := toStringIfOk(cacheDriverConfig, "region", "", log)
7071
allParametersOk = allParametersOk && ok
7172

72-
repoMetaTablename, ok := toStringIfOk(cacheDriverConfig, "repometatablename", "", log)
73+
tablenamePrefix, hasTablenamePrefix, ok := optionalStringIfOk(
74+
cacheDriverConfig, sconstants.DynamoDBTableNamePrefix, log)
7375
allParametersOk = allParametersOk && ok
7476

75-
repoBlobsInfoTablename, ok := toStringIfOk(cacheDriverConfig, "repoblobsinfotablename", "", log)
77+
repoMetaTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBRepoMetaTable,
78+
sconstants.DynamoDBRepoMetaSuffix, tablenamePrefix, hasTablenamePrefix, log)
7679
allParametersOk = allParametersOk && ok
7780

78-
imageMetaTablename, ok := toStringIfOk(cacheDriverConfig, "imagemetatablename", "", log)
81+
repoBlobsInfoTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBRepoBlobsTable,
82+
sconstants.DynamoDBRepoBlobsSuffix, tablenamePrefix, hasTablenamePrefix, log)
7983
allParametersOk = allParametersOk && ok
8084

81-
apiKeyTablename, ok := toStringIfOk(cacheDriverConfig, "apikeytablename", "", log)
85+
imageMetaTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBImageMetaTable,
86+
sconstants.DynamoDBImageMetaSuffix, tablenamePrefix, hasTablenamePrefix, log)
8287
allParametersOk = allParametersOk && ok
8388

84-
versionTablename, ok := toStringIfOk(cacheDriverConfig, "versiontablename", "", log)
89+
apiKeyTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBAPIKeyTable,
90+
sconstants.DynamoDBAPIKeySuffix, tablenamePrefix, hasTablenamePrefix, log)
8591
allParametersOk = allParametersOk && ok
8692

87-
userDataTablename, ok := toStringIfOk(cacheDriverConfig, "userdatatablename", "", log)
93+
versionTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBVersionTable,
94+
sconstants.DynamoDBVersionSuffix, tablenamePrefix, hasTablenamePrefix, log)
95+
allParametersOk = allParametersOk && ok
96+
97+
userDataTablename, ok := tableNameIfOk(cacheDriverConfig, sconstants.DynamoDBUserDataTable,
98+
sconstants.DynamoDBUserDataSuffix, tablenamePrefix, hasTablenamePrefix, log)
8899
allParametersOk = allParametersOk && ok
89100

90101
if !allParametersOk {
@@ -114,12 +125,47 @@ func getRedisParams(cacheDriverConfig map[string]any, log log.Logger) redis.DBDr
114125
}
115126
}
116127

128+
func tableNameIfOk(cacheDriverConfig map[string]any,
129+
param string,
130+
tableSuffix string,
131+
tablenamePrefix string,
132+
hasTablenamePrefix bool,
133+
log log.Logger,
134+
) (string, bool) {
135+
tableName, ok := configValue(cacheDriverConfig, param)
136+
if ok {
137+
return stringValueIfOk(tableName, param, log)
138+
}
139+
140+
if hasTablenamePrefix {
141+
return tablenamePrefix + tableSuffix, true
142+
}
143+
144+
log.Error().Str("field", param).Msg("failed to parse CacheDriver config, field is not present")
145+
146+
return "", false
147+
}
148+
149+
func optionalStringIfOk(cacheDriverConfig map[string]any,
150+
param string,
151+
log log.Logger,
152+
) (string, bool, bool) {
153+
val, ok := configValue(cacheDriverConfig, param)
154+
if !ok {
155+
return "", false, true
156+
}
157+
158+
str, ok := stringValueIfOk(val, param, log)
159+
160+
return str, true, ok
161+
}
162+
117163
func toStringIfOk(cacheDriverConfig map[string]any,
118164
param string,
119165
defaultVal string,
120166
log log.Logger,
121167
) (string, bool) {
122-
val, ok := cacheDriverConfig[param]
168+
val, ok := configValue(cacheDriverConfig, param)
123169

124170
if !ok && defaultVal != "" {
125171
log.Info().Str("field", param).Str("default", defaultVal).
@@ -132,6 +178,10 @@ func toStringIfOk(cacheDriverConfig map[string]any,
132178
return "", false
133179
}
134180

181+
return stringValueIfOk(val, param, log)
182+
}
183+
184+
func stringValueIfOk(val any, param string, log log.Logger) (string, bool) {
135185
str, ok := val.(string)
136186
if !ok {
137187
log.Error().Str("parameter", param).Msg("failed to parse CacheDriver config, parameter isn't a string")
@@ -148,6 +198,20 @@ func toStringIfOk(cacheDriverConfig map[string]any,
148198
return str, true
149199
}
150200

201+
func configValue(cacheDriverConfig map[string]any, key string) (any, bool) {
202+
if val, ok := cacheDriverConfig[key]; ok {
203+
return val, true
204+
}
205+
206+
for candidate, val := range cacheDriverConfig {
207+
if strings.EqualFold(candidate, key) {
208+
return val, true
209+
}
210+
}
211+
212+
return nil, false
213+
}
214+
151215
func Close(metadb mTypes.MetaDB) error {
152216
return metadb.Close()
153217
}

pkg/meta/meta_internal_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package meta
2+
3+
import (
4+
"testing"
5+
6+
"zotregistry.dev/zot/v2/pkg/log"
7+
)
8+
9+
func TestGetDynamoParamsWithTableNamePrefix(t *testing.T) {
10+
t.Parallel()
11+
12+
params := getDynamoParams(map[string]any{
13+
"endpoint": "http://localhost:4566",
14+
"region": "us-east-2",
15+
"tablenamePrefix": "Zot",
16+
}, log.NewTestLogger())
17+
18+
expectedTables := map[string]string{
19+
"RepoMetaTablename": "ZotRepoMetadataTable",
20+
"RepoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
21+
"ImageMetaTablename": "ZotImageMetaTable",
22+
"UserDataTablename": "ZotUserDataTable",
23+
"APIKeyTablename": "ZotApiKeyDataTable",
24+
"VersionTablename": "ZotVersionTable",
25+
}
26+
actualTables := map[string]string{
27+
"RepoMetaTablename": params.RepoMetaTablename,
28+
"RepoBlobsInfoTablename": params.RepoBlobsInfoTablename,
29+
"ImageMetaTablename": params.ImageMetaTablename,
30+
"UserDataTablename": params.UserDataTablename,
31+
"APIKeyTablename": params.APIKeyTablename,
32+
"VersionTablename": params.VersionTablename,
33+
}
34+
35+
for table, expected := range expectedTables {
36+
if actualTables[table] != expected {
37+
t.Fatalf("%s = %q, want %q", table, actualTables[table], expected)
38+
}
39+
}
40+
}
41+
42+
func TestGetDynamoParamsPrefersExplicitTableNames(t *testing.T) {
43+
t.Parallel()
44+
45+
params := getDynamoParams(map[string]any{
46+
"endpoint": "http://localhost:4566",
47+
"region": "us-east-2",
48+
"tablenamePrefix": "Zot",
49+
"imageMetaTablename": "CustomImageMetaTable",
50+
}, log.NewTestLogger())
51+
52+
if params.ImageMetaTablename != "CustomImageMetaTable" {
53+
t.Fatalf("ImageMetaTablename = %q, want %q", params.ImageMetaTablename, "CustomImageMetaTable")
54+
}
55+
56+
if params.RepoMetaTablename != "ZotRepoMetadataTable" {
57+
t.Fatalf("RepoMetaTablename = %q, want %q", params.RepoMetaTablename, "ZotRepoMetadataTable")
58+
}
59+
}
60+
61+
func TestGetDynamoParamsRejectsInvalidTableNamePrefix(t *testing.T) {
62+
t.Parallel()
63+
64+
tests := map[string]any{
65+
"empty": "",
66+
"nonString": false,
67+
}
68+
69+
for name, prefix := range tests {
70+
name := name
71+
prefix := prefix
72+
73+
t.Run(name, func(t *testing.T) {
74+
t.Parallel()
75+
76+
requirePanic(t, func() {
77+
getDynamoParams(map[string]any{
78+
"endpoint": "http://localhost:4566",
79+
"region": "us-east-2",
80+
"tablenamePrefix": prefix,
81+
}, log.NewTestLogger())
82+
})
83+
})
84+
}
85+
}
86+
87+
func requirePanic(t *testing.T, fn func()) {
88+
t.Helper()
89+
90+
defer func() {
91+
if recover() == nil {
92+
t.Fatal("function did not panic")
93+
}
94+
}()
95+
96+
fn()
97+
}

pkg/storage/cache.go

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package storage
22

33
import (
44
"fmt"
5+
"strings"
56

67
zerr "zotregistry.dev/zot/v2/errors"
78
"zotregistry.dev/zot/v2/pkg/api/config"
@@ -97,19 +98,67 @@ func getDynamoParams(storageConfig *config.StorageConfig) (cache.DynamoDBDriverP
9798
dynamoParams := cache.DynamoDBDriverParameters{}
9899
dynamoParams.Endpoint, _ = storageConfig.CacheDriver["endpoint"].(string)
99100
dynamoParams.Region, _ = storageConfig.CacheDriver["region"].(string)
100-
dynamoParams.TableName, _ = storageConfig.CacheDriver["cachetablename"].(string)
101101

102-
cachetable, ok := storageConfig.CacheDriver["cachetablename"]
102+
tableName, err := getDynamoTableName(
103+
storageConfig.CacheDriver,
104+
constants.DynamoDBCacheTableName,
105+
constants.DynamoDBCacheTableSuffix,
106+
)
107+
if err != nil {
108+
return dynamoParams, err
109+
}
110+
111+
dynamoParams.TableName = tableName
112+
113+
return dynamoParams, nil
114+
}
115+
116+
func getDynamoTableName(cacheDriverConfig map[string]any, tableKey, tableSuffix string) (string, error) {
117+
tableName, ok := cacheDriverConfigValue(cacheDriverConfig, tableKey)
118+
if ok {
119+
tableValue, ok := tableName.(string)
120+
if !ok {
121+
return "", fmt.Errorf("%w: failed to cast %s %v to string type", zerr.ErrBadConfig, tableKey, tableName)
122+
}
123+
124+
if tableValue == "" {
125+
return "", fmt.Errorf("%w: %s key is empty", zerr.ErrBadConfig, tableKey)
126+
}
127+
128+
return tableValue, nil
129+
}
130+
131+
prefix, ok := cacheDriverConfigValue(cacheDriverConfig, constants.DynamoDBTableNamePrefix)
103132
if !ok {
104-
return dynamoParams, fmt.Errorf("%w: cachetablename key is mandatory for dynamodb cache driver", zerr.ErrBadConfig)
133+
return "", fmt.Errorf("%w: %s key or %s key is mandatory for dynamodb cache driver",
134+
zerr.ErrBadConfig, tableKey, constants.DynamoDBTableNamePrefix)
105135
}
106136

107-
dynamoParams.TableName, ok = cachetable.(string)
137+
prefixValue, ok := prefix.(string)
108138
if !ok {
109-
return dynamoParams, fmt.Errorf("%w: failed to cast cachetablename %s to string type", zerr.ErrBadConfig, cachetable)
139+
return "", fmt.Errorf("%w: failed to cast %s %v to string type",
140+
zerr.ErrBadConfig, constants.DynamoDBTableNamePrefix, prefix)
110141
}
111142

112-
return dynamoParams, nil
143+
if prefixValue == "" {
144+
return "", fmt.Errorf("%w: %s key is empty", zerr.ErrBadConfig, constants.DynamoDBTableNamePrefix)
145+
}
146+
147+
return prefixValue + tableSuffix, nil
148+
}
149+
150+
func cacheDriverConfigValue(cacheDriverConfig map[string]any, key string) (any, bool) {
151+
if val, ok := cacheDriverConfig[key]; ok {
152+
return val, true
153+
}
154+
155+
for candidate, val := range cacheDriverConfig {
156+
if strings.EqualFold(candidate, key) {
157+
return val, true
158+
}
159+
}
160+
161+
return nil, false
113162
}
114163

115164
func getRedisParams(storageConfig *config.StorageConfig, log zlog.Logger) (cache.RedisDriverParameters, error) {

0 commit comments

Comments
 (0)