Skip to content

Commit d1b8dd7

Browse files
darkweakkresike
andauthored
fix(providers): Sanitize nuts properties from configuration (#221)
* fix(providers): Sanitize nuts properties from configuration * Support cache_keys JSON configuration * keep stale on purge if strategy is not hard * Fix stale surrogate invalidation * Update E2E tests * Group configuration object * Surrogate_keys enhancement * Fix surrogate with commas * Support the etcd provider * Fix nutsdb regex based cache purge. (#222) * Handle delete many by regexp on etcd provider * Increase wait for Chi & Webgo run to 30s * Remove Yoda conditions * Remove useless print Co-authored-by: Kiss Károly Pál <[email protected]>
1 parent 602a4e1 commit d1b8dd7

File tree

362 files changed

+170146
-739
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

362 files changed

+170146
-739
lines changed

.github/workflows/plugins-master.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ jobs:
2424
uses: actions/checkout@v2
2525
-
2626
name: Install xcaddy
27-
run: |
28-
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
29-
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | sudo apt-key add -
30-
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-xcaddy.list
31-
sudo apt update
32-
sudo apt install xcaddy
27+
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
3328
-
3429
name: Build current Souin as caddy module with referenced Souin core version when merge on master
3530
run: cd plugins/caddy && xcaddy build --with github.com/${{ github.repository }}/plugins/caddy@$(git rev-parse --short "$GITHUB_SHA")

.github/workflows/plugins.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ jobs:
144144
name: Wait for Souin is really loaded inside Chi as middleware
145145
uses: jakejarvis/wait-action@master
146146
with:
147-
time: 20s
147+
time: 30s
148148
-
149149
name: Set Chi logs configuration result as environment variable
150150
run: cd plugins/chi/examples && echo "CHI_MIDDLEWARE_RESULT=$(docker-compose logs chi | grep Souin)" >> $GITHUB_ENV
@@ -531,7 +531,7 @@ jobs:
531531
name: Wait for Souin is really loaded inside Webgo as middleware
532532
uses: jakejarvis/wait-action@master
533533
with:
534-
time: 20s
534+
time: 30s
535535
-
536536
name: Set Webgo logs configuration result as environment variable
537537
run: cd plugins/webgo/examples && echo "WEBGO_MIDDLEWARE_RESULT=$(docker-compose logs webgo | grep Souin)" >> $GITHUB_ENV

README.md

+31-5
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,20 @@ default_cache:
9090
- GET
9191
- POST
9292
- HEAD
93-
distributed: true # Use Olric distributed storage
93+
distributed: true # Use Olric or Etcd distributed storage
9494
headers: # Default headers concatenated in stored keys
9595
- Authorization
9696
key:
9797
disable_body: true
9898
disable_host: true
9999
disable_method: true
100-
olric: # If distributed is set to true, you'll have to define the olric section
100+
etcd: # If distributed is set to true, you'll have to define either the etcd or olric section
101+
configuration: # Configure directly the Etcd client
102+
endpoints: # Define multiple endpoints
103+
- etcd-1:2379 # First node
104+
- etcd-2:2379 # Second node
105+
- etcd-3:2379 # Third node
106+
olric: # If distributed is set to true, you'll have to define either the etcd or olric section
101107
url: 'olric:3320' # Olric server
102108
regex:
103109
exclude: 'ARegexHere' # Regex to exclude from cache
@@ -171,6 +177,8 @@ surrogate_keys:
171177
| `default_cache.key.disable_body` | Disable the body part in the key (GraphQL context) | `true`<br/><br/>`(default: false)` |
172178
| `default_cache.key.disable_host` | Disable the host part in the key | `true`<br/><br/>`(default: false)` |
173179
| `default_cache.key.disable_method` | Disable the method part in the key | `true`<br/><br/>`(default: false)` |
180+
| `default_cache.etcd` | Configure the Etcd cache storage | |
181+
| `default_cache.etcd.configuration` | Configure Etcd directly in the Caddyfile or your JSON caddy configuration | [See the Etcd configuration for the options](https://pkg.go.dev/go.etcd.io/etcd/clientv3#Config) |
174182
| `default_cache.olric` | Configure the Olric cache storage | |
175183
| `default_cache.olric.path` | Configure Olric with a file | `/anywhere/olric_configuration.json` |
176184
| `default_cache.olric.configuration` | Configure Olric directly in the Caddyfile or your JSON caddy configuration | [See the Olric configuration for the options](https://github.com/buraksezer/olric/blob/master/cmd/olricd/olricd.yaml/) |
@@ -243,9 +251,10 @@ See the sequence diagram for the minimal version below
243251
Supported providers
244252
- [Badger](https://github.com/dgraph-io/badger)
245253
- [NutsDB](https://github.com/nutsdb/nutsdb)
254+
- [Etcd](https://github.com/etcd-io/etcd)
246255
- [Olric](https://github.com/buraksezer/olric)
247256

248-
The cache system sits on top of three providers at the moment. It provides two in-memory storage solutions (badger and nuts), and a distributed storage called Olric because setting, getting, updating and deleting keys in these providers is as easy as it gets.
257+
The cache system sits on top of three providers at the moment. It provides two in-memory storage solutions (badger and nuts), and two distributed storages Olric and Etcd because setting, getting, updating and deleting keys in these providers is as easy as it gets.
249258
**The Badger provider (default one)**: you can tune its configuration using the badger configuration inside your Souin configuration. In order to do that, you have to declare the `badger` block. See the following json example.
250259
```json
251260
"badger": {
@@ -284,8 +293,19 @@ The cache system sits on top of three providers at the moment. It provides two i
284293
}
285294
```
286295
In order to do that, the Olric provider need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible.
296+
297+
**The Etcd provider**: you can tune its configuration using the etcd configuration inside your Souin configuration and declare Souin has to use the distributed provider. In order to do that, you have to declare the `etcd` block and the `distributed` directive. See the following json example.
298+
```json
299+
"distributed": true,
300+
"etcd": {
301+
"configuration": {
302+
# Etcd configuration here...
303+
}
304+
}
305+
```
306+
In order to do that, the Etcd provider need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible.
287307
Souin will return at first the response from the choosen provider when it gives a non-empty response, or fallback to the reverse proxy otherwise.
288-
Since v1.4.2, Souin supports [Olric](https://github.com/buraksezer/olric) to handle distributed cache.
308+
Since v1.4.2, Souin supports [Olric](https://github.com/buraksezer/olric) and since v1.6.10 it supports [Etcd](https://github.com/etcd-io/etcd) to handle distributed cache.
289309

290310
## GraphQL
291311
This feature is currently in beta.
@@ -435,11 +455,16 @@ There is the fully configuration below
435455
disable_method
436456
}
437457
log_level debug
458+
etcd {
459+
configuration {
460+
# Your Etcd configuration here
461+
}
462+
}
438463
olric {
439464
url url_to_your_cluster:3320
440465
path the_path_to_a_file.yaml
441466
configuration {
442-
# Your badger configuration here
467+
# Your Olric configuration here
443468
}
444469
}
445470
regex {
@@ -918,3 +943,4 @@ Thanks to these users for contributing or helping this project in any way
918943
* [Menci](https://github.com/menci)
919944
* [Duy Nguyen](https://github.com/duy-nguyen-devops)
920945
* [Kiss Karoly](https://github.com/kresike)
946+
* [Matthias von Bargen](https://github.com/mattvb91)

cache/providers/abstractProvider.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ const stalePrefix = "STALE_"
1616
func InitializeProvider(configuration configurationtypes.AbstractConfigurationInterface) types.AbstractProviderInterface {
1717
var r types.AbstractProviderInterface
1818
if configuration.GetDefaultCache().GetDistributed() {
19-
if configuration.GetDefaultCache().GetOlric().URL != "" {
20-
r, _ = OlricConnectionFactory(configuration)
19+
if configuration.GetDefaultCache().GetEtcd().Configuration != nil {
20+
r, _ = EtcdConnectionFactory(configuration)
2121
} else {
22-
r, _ = EmbeddedOlricConnectionFactory(configuration)
22+
if configuration.GetDefaultCache().GetOlric().URL != "" {
23+
r, _ = OlricConnectionFactory(configuration)
24+
} else {
25+
r, _ = EmbeddedOlricConnectionFactory(configuration)
26+
}
2327
}
2428
} else if configuration.GetDefaultCache().GetNuts().Configuration != nil || configuration.GetDefaultCache().GetNuts().Path != "" {
2529
r, _ = NutsConnectionFactory(configuration)
@@ -46,10 +50,12 @@ func varyVoter(baseKey string, req *http.Request, currentKey string) bool {
4650

4751
for _, item := range strings.Split(list, ";") {
4852
index := strings.LastIndex(item, ":")
49-
if len(item) >= index+1 && strings.Contains(req.Header.Get(item[:index]), item[index+1:]) {
50-
return true
53+
if !(len(item) >= index+1 && req.Header.Get(item[:index]) == item[index+1:]) {
54+
return false
5155
}
5256
}
57+
58+
return true
5359
}
5460

5561
return false

cache/providers/abstractProvider_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
package providers
22

33
import (
4+
"fmt"
45
"testing"
6+
"time"
57

8+
"github.com/darkweak/souin/cache/types"
9+
"github.com/darkweak/souin/configurationtypes"
610
"github.com/darkweak/souin/errors"
711
"github.com/darkweak/souin/tests"
812
)
913

14+
const BYTEKEY = "MyByteKey"
15+
const NONEXISTENTKEY = "NonexistentKey"
16+
17+
func verifyNewValueAfterSet(client types.AbstractProviderInterface, key string, value []byte, t *testing.T) {
18+
newValue := client.Get(key)
19+
20+
if len(newValue) != len(value) {
21+
errors.GenerateError(t, fmt.Sprintf("Key %s should be equals to %s, %s provided", key, value, newValue))
22+
}
23+
}
24+
25+
func setValueThenVerify(client types.AbstractProviderInterface, key string, value []byte, matchedURL configurationtypes.URL, ttl time.Duration, t *testing.T) {
26+
client.Set(key, value, matchedURL, ttl)
27+
time.Sleep(1 * time.Second)
28+
verifyNewValueAfterSet(client, key, value, t)
29+
}
30+
1031
func TestInitializeProvider(t *testing.T) {
1132
c := tests.MockConfiguration(tests.BaseConfiguration)
1233
p := InitializeProvider(c)

cache/providers/badgerProvider.go

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func (provider *Badger) Prefix(key string, req *http.Request) []byte {
112112
defer it.Close()
113113
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
114114
if varyVoter(key, req, string(it.Item().Key())) {
115+
fmt.Println(string(it.Item().Key()))
115116
_ = it.Item().Value(func(val []byte) error {
116117
result = val
117118
return nil

cache/providers/badgerProvider_test.go

+1-17
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import (
1414
)
1515

1616
const BADGERVALUE = "My first data"
17-
const BYTEKEY = "MyByteKey"
18-
const NONEXISTENTKEY = "NonexistentKey"
1917

2018
func getBadgerClientAndMatchedURL(key string) (types.AbstractProviderInterface, configurationtypes.URL) {
2119
return tests.GetCacheProviderClientAndMatchedURL(
@@ -89,7 +87,7 @@ func TestBadger_GetSetRequestInCache_OneByte(t *testing.T) {
8987
time.Sleep(1 * time.Second)
9088

9189
res := client.Get(BYTEKEY)
92-
if 0 == len(res) {
90+
if len(res) == 0 {
9391
errors.GenerateError(t, fmt.Sprintf("Key %s should exist", BYTEKEY))
9492
}
9593

@@ -98,20 +96,6 @@ func TestBadger_GetSetRequestInCache_OneByte(t *testing.T) {
9896
}
9997
}
10098

101-
func verifyNewValueAfterSet(client types.AbstractProviderInterface, key string, value []byte, t *testing.T) {
102-
newValue := client.Get(key)
103-
104-
if len(newValue) != len(value) {
105-
errors.GenerateError(t, fmt.Sprintf("Key %s should be equals to %s, %s provided", key, value, newValue))
106-
}
107-
}
108-
109-
func setValueThenVerify(client types.AbstractProviderInterface, key string, value []byte, matchedURL configurationtypes.URL, ttl time.Duration, t *testing.T) {
110-
client.Set(key, value, matchedURL, ttl)
111-
time.Sleep(1 * time.Second)
112-
verifyNewValueAfterSet(client, key, value, t)
113-
}
114-
11599
func TestBadger_SetRequestInCache_TTL(t *testing.T) {
116100
key := "MyEmptyKey"
117101
client, matchedURL := getBadgerClientAndMatchedURL(key)

cache/providers/etcdProvider.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package providers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"regexp"
9+
"time"
10+
11+
t "github.com/darkweak/souin/configurationtypes"
12+
clientv3 "go.etcd.io/etcd/client/v3"
13+
)
14+
15+
// Etcd provider type
16+
type Etcd struct {
17+
*clientv3.Client
18+
stale time.Duration
19+
ctx context.Context
20+
}
21+
22+
// EtcdConnectionFactory function create new Nuts instance
23+
func EtcdConnectionFactory(c t.AbstractConfigurationInterface) (*Etcd, error) {
24+
dc := c.GetDefaultCache()
25+
bc, _ := json.Marshal(dc.GetEtcd().Configuration)
26+
etcdConfiguration := clientv3.Config{
27+
DialTimeout: 5 * time.Second,
28+
AutoSyncInterval: 1 * time.Second,
29+
Logger: c.GetLogger(),
30+
}
31+
_ = json.Unmarshal(bc, &etcdConfiguration)
32+
33+
cli, err := clientv3.New(etcdConfiguration)
34+
35+
if err != nil {
36+
fmt.Println("Impossible to initialize the Etcd DB.", err)
37+
return nil, err
38+
}
39+
40+
return &Etcd{
41+
Client: cli,
42+
ctx: context.Background(),
43+
stale: dc.GetStale(),
44+
}, nil
45+
}
46+
47+
// ListKeys method returns the list of existing keys
48+
func (provider *Etcd) ListKeys() []string {
49+
keys := []string{}
50+
51+
r, e := provider.Client.Get(provider.ctx, "\x00", clientv3.WithFromKey())
52+
53+
if e != nil {
54+
return []string{}
55+
}
56+
for _, k := range r.Kvs {
57+
keys = append(keys, string(k.Key))
58+
}
59+
60+
return keys
61+
}
62+
63+
// Get method returns the populated response if exists, empty response then
64+
func (provider *Etcd) Get(key string) (item []byte) {
65+
r, e := provider.Client.Get(provider.ctx, key)
66+
67+
if e == nil && r != nil && len(r.Kvs) > 0 {
68+
item = r.Kvs[0].Value
69+
}
70+
71+
return
72+
}
73+
74+
// Prefix method returns the populated response if exists, empty response then
75+
func (provider *Etcd) Prefix(key string, req *http.Request) []byte {
76+
r, e := provider.Client.Get(provider.ctx, key, clientv3.WithPrefix())
77+
78+
if e == nil && r != nil {
79+
for _, v := range r.Kvs {
80+
if varyVoter(key, req, string(v.Key)) {
81+
return v.Value
82+
}
83+
}
84+
}
85+
86+
return []byte{}
87+
}
88+
89+
// Set method will store the response in Etcd provider
90+
func (provider *Etcd) Set(key string, value []byte, url t.URL, duration time.Duration) {
91+
if duration == 0 {
92+
duration = url.TTL.Duration
93+
}
94+
95+
rs, _ := provider.Client.Grant(context.TODO(), int64(duration.Seconds()))
96+
_, err := provider.Client.Put(provider.ctx, key, string(value), clientv3.WithLease(rs.ID))
97+
98+
if err != nil {
99+
panic(fmt.Sprintf("Impossible to set value into Etcd, %s", err))
100+
}
101+
102+
_, err = provider.Client.Put(provider.ctx, stalePrefix+key, string(value), clientv3.WithLease(rs.ID))
103+
104+
if err != nil {
105+
panic(fmt.Sprintf("Impossible to set value into Etcd, %s", err))
106+
}
107+
}
108+
109+
// Delete method will delete the response in Etcd provider if exists corresponding to key param
110+
func (provider *Etcd) Delete(key string) {
111+
_, _ = provider.Client.Delete(provider.ctx, key)
112+
}
113+
114+
// DeleteMany method will delete the responses in Nuts provider if exists corresponding to the regex key param
115+
func (provider *Etcd) DeleteMany(key string) {
116+
re, e := regexp.Compile(key)
117+
118+
if e != nil {
119+
return
120+
}
121+
122+
if r, e := provider.Client.Get(provider.ctx, "\x00", clientv3.WithFromKey()); e == nil {
123+
for _, k := range r.Kvs {
124+
key := string(k.Key)
125+
if re.MatchString(key) {
126+
provider.Delete(key)
127+
}
128+
}
129+
}
130+
}
131+
132+
// Init method will
133+
func (provider *Etcd) Init() error {
134+
return nil
135+
}
136+
137+
// Reset method will reset or close provider
138+
func (provider *Etcd) Reset() error {
139+
return provider.Client.Close()
140+
}

0 commit comments

Comments
 (0)