diff --git a/.github/workflows/non-regression.yml b/.github/workflows/non-regression.yml index 6f94b95c4..248d964bd 100644 --- a/.github/workflows/non-regression.yml +++ b/.github/workflows/non-regression.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v3 with: @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v3 with: @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v3 with: @@ -59,7 +59,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build the stack run: docker network create your_network || true diff --git a/.github/workflows/plugin_template.yml b/.github/workflows/plugin_template.yml index d856fd8f5..b909ddd63 100644 --- a/.github/workflows/plugin_template.yml +++ b/.github/workflows/plugin_template.yml @@ -31,7 +31,7 @@ jobs: go-version: ${{ inputs.GO_VERSION }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/plugins-master.yml b/.github/workflows/plugins-master.yml index bf92159d2..f07587242 100644 --- a/.github/workflows/plugins-master.yml +++ b/.github/workflows/plugins-master.yml @@ -24,7 +24,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install xcaddy run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index f00e24e9b..66d480c7e 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -39,7 +39,7 @@ jobs: go-version: '1.21' - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install xcaddy run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c10e4ab8..72b4c4818 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -48,7 +48,7 @@ jobs: run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -85,7 +85,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - @@ -113,7 +113,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate Tyk amd64 artifacts run: cd plugins/tyk && make vendor && docker compose -f docker-compose.yml.artifacts up diff --git a/.github/workflows/workflow_plugins_generator.sh b/.github/workflows/workflow_plugins_generator.sh index 2536ac9be..a2f80ff75 100644 --- a/.github/workflows/workflow_plugins_generator.sh +++ b/.github/workflows/workflow_plugins_generator.sh @@ -45,7 +45,7 @@ jobs: go-version: '$go_version' - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install xcaddy run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest diff --git a/plugins/roadrunner/examples/Dockerfile.test b/plugins/roadrunner/examples/Dockerfile.test index 4bc23e71c..09d79a494 100644 --- a/plugins/roadrunner/examples/Dockerfile.test +++ b/plugins/roadrunner/examples/Dockerfile.test @@ -3,7 +3,7 @@ FROM ghcr.io/roadrunner-server/velox:latest as velox ARG CURRENT_SHA ARG GH_TOKEN ENV CGO_ENABLED=0 -ENV VERSION=v2023.2.2 +ENV VERSION=v2023.3.0-beta.2 ENV TIME="$(date +%H:%M)" RUN apk add git diff --git a/plugins/roadrunner/examples/configuration.toml b/plugins/roadrunner/examples/configuration.toml index 7a2633381..b0f3193c2 100644 --- a/plugins/roadrunner/examples/configuration.toml +++ b/plugins/roadrunner/examples/configuration.toml @@ -6,7 +6,7 @@ build_args = [ ] [roadrunner] -ref = "v2023.2.2" +ref = "v2023.3.0-beta.2" [github] [github.token] @@ -15,9 +15,9 @@ token = "GH_TOKEN" [github.plugins] logger = { ref = "v4.2.1", owner = "roadrunner-server", repository = "logger" } cache = { ref = "CURRENT_SHA", owner = "darkweak", repository = "souin", folder = "/plugins/roadrunner", replace = "/opt/plugins/roadrunner" } -server = { ref = "v4.1.4", owner = "roadrunner-server", repository = "server" } -gzip = { ref = "v4.0.7", owner = "roadrunner-server", repository = "gzip" } -http = { ref = "v4.1.7", owner = "roadrunner-server", repository = "http" } +server = { ref = "v4.3.1", owner = "roadrunner-server", repository = "server" } +gzip = { ref = "v4.1.2", owner = "roadrunner-server", repository = "gzip" } +http = { ref = "v4.3.1", owner = "roadrunner-server", repository = "http" } [log] level = "debug" diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go index d4feddd91..010677f12 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go @@ -29,8 +29,10 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S if err != nil { panic(err) } + fmt.Println("Storers initialized.") regexpUrls := helpers.InitializeRegexp(c) surrogateStorage := surrogate.InitializeSurrogate(c) + fmt.Println("Surrogate storage initialized.") var excludedRegexp *regexp.Regexp = nil if c.GetDefaultCache().GetRegex().Exclude != "" { excludedRegexp = regexp.MustCompile(c.GetDefaultCache().GetRegex().Exclude) @@ -49,6 +51,7 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S Headers: c.GetDefaultCache().GetHeaders(), DefaultCacheControl: c.GetDefaultCache().GetDefaultCacheControl(), } + fmt.Println("Souin configuration is now loaded.") return &SouinBaseHandler{ Configuration: c, @@ -66,7 +69,6 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S type SouinBaseHandler struct { Configuration configurationtypes.AbstractConfigurationInterface - Storer storage.Storer Storers []storage.Storer InternalEndpointHandlers *api.MapHandler ExcludeRegex *regexp.Regexp diff --git a/plugins/tyk/main.go b/plugins/tyk/main.go index 0eb6ec525..7f807e548 100644 --- a/plugins/tyk/main.go +++ b/plugins/tyk/main.go @@ -107,10 +107,32 @@ func SouinResponseHandler(rw http.ResponseWriter, rs *http.Response, rq *http.Re if err == nil { variedHeaders := rfc.HeaderAllCommaSepValues(res.Header) cachedKey += rfc.GetVariedCacheKey(rq, variedHeaders) - if s.SouinBaseHandler.Storer.Set(cachedKey, response, currentMatchedURL, ma) == nil { + var wg sync.WaitGroup + mu := sync.Mutex{} + fails := []string{} + for _, storer := range s.SouinBaseHandler.Storers { + wg.Add(1) + go func(currentStorer storage.Storer) { + defer wg.Done() + if currentStorer.Set(cachedKey, response, currentMatchedURL, ma) == nil { + } else { + mu.Lock() + fails = append(fails, fmt.Sprintf("; detail=%s-INSERTION-ERROR", currentStorer.Name())) + mu.Unlock() + } + }(storer) + } + + wg.Wait() + if len(fails) < len(s.SouinBaseHandler.Storers) { + go func(rs http.Response, key string) { + _ = s.SurrogateKeyStorer.Store(&rs, key) + }(res, cachedKey) status += "; stored" - } else { - status += "; detail=STORAGE-INSERTION-ERROR" + } + + if len(fails) > 0 { + status += strings.Join(fails, "") } } } else { @@ -157,7 +179,13 @@ func SouinRequestHandler(rw http.ResponseWriter, rq *http.Request) { defer s.bufPool.Put(bufPool) if !requestCc.NoCache { validator := rfc.ParseRequest(rq) - response := s.SouinBaseHandler.Storer.Prefix(cachedKey, rq, validator) + var response *http.Response + for _, currentStorer := range s.SouinBaseHandler.Storers { + response = currentStorer.Prefix(cachedKey, rq, validator) + if response != nil { + break + } + } if response != nil && rfc.ValidateCacheControl(response, requestCc) { rfc.SetCacheStatusHeader(response) @@ -170,7 +198,12 @@ func SouinRequestHandler(rw http.ResponseWriter, rq *http.Request) { return } } else if response == nil && (requestCc.MaxStaleSet || requestCc.MaxStale > -1) { - response := s.SouinBaseHandler.Storer.Prefix(storage.StalePrefix+cachedKey, rq, validator) + for _, currentStorer := range s.SouinBaseHandler.Storers { + response = currentStorer.Prefix(storage.StalePrefix+cachedKey, rq, validator) + if response != nil { + break + } + } if nil != response && rfc.ValidateCacheControl(response, requestCc) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) rfc.SetCacheStatusHeader(response) diff --git a/plugins/tyk/override/api/main.go b/plugins/tyk/override/api/main.go index a5d66938a..4625aa0b8 100644 --- a/plugins/tyk/override/api/main.go +++ b/plugins/tyk/override/api/main.go @@ -16,7 +16,7 @@ type MapHandler struct { // GenerateHandlerMap generate the MapHandler func GenerateHandlerMap( configuration configurationtypes.AbstractConfigurationInterface, - storer storage.Storer, + storers []storage.Storer, surrogateStorage providers.SurrogateInterface, ) *MapHandler { hm := make(map[string]http.HandlerFunc) @@ -28,12 +28,10 @@ func GenerateHandlerMap( basePathAPIS = "/souin-api" } - for _, endpoint := range Initialize(configuration, storer, surrogateStorage) { + for _, endpoint := range Initialize(configuration, storers, surrogateStorage) { if endpoint.IsEnabled() { shouldEnable = true - if e, ok := endpoint.(*SouinAPI); ok { - hm[basePathAPIS+endpoint.GetBasePath()] = e.HandleRequest - } + hm[basePathAPIS+endpoint.GetBasePath()] = endpoint.HandleRequest } } @@ -45,6 +43,6 @@ func GenerateHandlerMap( } // Initialize contains all apis that should be enabled -func Initialize(c configurationtypes.AbstractConfigurationInterface, storer storage.Storer, surrogateStorage providers.SurrogateInterface) []EndpointInterface { - return []EndpointInterface{initializeSouin(c, storer, surrogateStorage)} +func Initialize(c configurationtypes.AbstractConfigurationInterface, storers []storage.Storer, surrogateStorage providers.SurrogateInterface) []EndpointInterface { + return []EndpointInterface{initializeSouin(c, storers, surrogateStorage)} } diff --git a/plugins/tyk/override/api/souin.go b/plugins/tyk/override/api/souin.go index 81c72f68d..5a5fb3279 100644 --- a/plugins/tyk/override/api/souin.go +++ b/plugins/tyk/override/api/souin.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "regexp" + "strings" "github.com/darkweak/souin/configurationtypes" "github.com/darkweak/souin/pkg/storage" @@ -15,40 +16,73 @@ import ( type SouinAPI struct { basePath string enabled bool - storer storage.Storer + storers []storage.Storer surrogateStorage providers.SurrogateInterface + allowedMethods []string +} + +type invalidationType string + +const ( + uriInvalidationType invalidationType = "uri" + uriPrefixInvalidationType invalidationType = "uri-prefix" + originInvalidationType invalidationType = "origin" + groupInvalidationType invalidationType = "group" +) + +type invalidation struct { + Type invalidationType `json:"type"` + Selectors []string `json:"selectors"` + Groups []string `json:"groups"` + Purge bool `json:"purge"` } func initializeSouin( configuration configurationtypes.AbstractConfigurationInterface, - storer storage.Storer, + storers []storage.Storer, surrogateStorage providers.SurrogateInterface, ) *SouinAPI { basePath := configuration.GetAPI().Souin.BasePath if basePath == "" { basePath = "/souin" } + + allowedMethods := configuration.GetDefaultCache().GetAllowedHTTPVerbs() + if len(allowedMethods) == 0 { + allowedMethods = []string{http.MethodGet, http.MethodHead} + } + return &SouinAPI{ basePath, configuration.GetAPI().Souin.Enable, - storer, + storers, surrogateStorage, + allowedMethods, } } // BulkDelete allow user to delete multiple items with regexp func (s *SouinAPI) BulkDelete(key string) { - s.storer.DeleteMany(key) + for _, current := range s.storers { + current.DeleteMany(key) + } } // Delete will delete a record into the provider cache system and will update the Souin API if enabled func (s *SouinAPI) Delete(key string) { - s.storer.Delete(key) + for _, current := range s.storers { + current.Delete(key) + } } // GetAll will retrieve all stored keys in the provider func (s *SouinAPI) GetAll() []string { - return s.storer.ListKeys() + keys := []string{} + for _, current := range s.storers { + keys = append(keys, current.ListKeys()...) + } + + return keys } // GetBasePath will return the basepath for this resource @@ -94,13 +128,87 @@ func (s *SouinAPI) HandleRequest(w http.ResponseWriter, r *http.Request) { res, _ = json.Marshal(s.GetAll()) } w.Header().Set("Content-Type", "application/json") + case http.MethodPost: + var invalidator invalidation + err := json.NewDecoder(r.Body).Decode(&invalidator) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + keysToInvalidate := []string{} + switch invalidator.Type { + case groupInvalidationType: + keysToInvalidate, _ = s.surrogateStorage.Purge(http.Header{"Surrogate-Key": invalidator.Groups}) + case uriPrefixInvalidationType, uriInvalidationType: + bodyKeys := []string{} + listedKeys := s.GetAll() + for _, k := range invalidator.Selectors { + if !strings.Contains(k, "//") { + rq, err := http.NewRequest(http.MethodGet, "//"+k, nil) + if err != nil { + continue + } + + bodyKeys = append(bodyKeys, rq.Host+"-"+rq.URL.Path) + } + } + + for _, allKey := range listedKeys { + for _, bk := range bodyKeys { + if invalidator.Type == uriInvalidationType { + if strings.Contains(allKey, bk) && strings.Contains(allKey, bk+"-") && strings.HasSuffix(allKey, bk) { + keysToInvalidate = append(keysToInvalidate, allKey) + break + } + } else { + if strings.Contains(allKey, bk) && + (strings.Contains(allKey, bk+"-") || strings.Contains(allKey, bk+"?") || strings.Contains(allKey, bk+"/") || strings.HasSuffix(allKey, bk)) { + keysToInvalidate = append(keysToInvalidate, allKey) + break + } + } + } + } + case originInvalidationType: + bodyKeys := []string{} + listedKeys := s.GetAll() + for _, k := range invalidator.Selectors { + if !strings.Contains(k, "//") { + rq, err := http.NewRequest(http.MethodGet, "//"+k, nil) + if err != nil { + continue + } + + bodyKeys = append(bodyKeys, rq.Host) + } + } + + for _, allKey := range listedKeys { + for _, bk := range bodyKeys { + if strings.Contains(allKey, bk) { + keysToInvalidate = append(keysToInvalidate, allKey) + break + } + } + } + } + + for _, k := range keysToInvalidate { + for _, current := range s.storers { + current.Delete(k) + } + } + w.WriteHeader(http.StatusOK) case "PURGE": if compile { keysRg := regexp.MustCompile(s.GetBasePath() + "/(.+)") flushRg := regexp.MustCompile(s.GetBasePath() + "/flush$") if flushRg.FindString(r.RequestURI) != "" { - s.storer.DeleteMany(".+") + for _, current := range s.storers { + current.DeleteMany(".+") + } e := s.surrogateStorage.Destruct() if e != nil { fmt.Printf("Error while purging the surrogate keys: %+v.", e) @@ -113,7 +221,9 @@ func (s *SouinAPI) HandleRequest(w http.ResponseWriter, r *http.Request) { } else { ck, _ := s.surrogateStorage.Purge(r.Header) for _, k := range ck { - s.storer.Delete(k) + for _, current := range s.storers { + current.Delete(k) + } } } w.WriteHeader(http.StatusNoContent) diff --git a/plugins/tyk/override/middleware/middleware.go b/plugins/tyk/override/middleware/middleware.go index 6ec15285b..26d50a6dd 100644 --- a/plugins/tyk/override/middleware/middleware.go +++ b/plugins/tyk/override/middleware/middleware.go @@ -55,7 +55,7 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S c.SetLogger(logger) } - storer, err := storage.NewStorage(c) + storers, err := storage.NewStorages(c) if err != nil { panic(err) } @@ -85,20 +85,21 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S return &SouinBaseHandler{ Configuration: c, - Storer: storer, - InternalEndpointHandlers: api.GenerateHandlerMap(c, storer, surrogateStorage), + Storers: storers, + InternalEndpointHandlers: api.GenerateHandlerMap(c, storers, surrogateStorage), ExcludeRegex: excludedRegexp, RegexpUrls: regexpUrls, DefaultMatchedUrl: defaultMatchedUrl, SurrogateKeyStorer: surrogateStorage, context: ctx, bufPool: bufPool, + storersLen: len(storers), } } type SouinBaseHandler struct { Configuration configurationtypes.AbstractConfigurationInterface - Storer storage.Storer + Storers []storage.Storer InternalEndpointHandlers *api.MapHandler ExcludeRegex *regexp.Regexp RegexpUrls regexp.Regexp @@ -107,6 +108,7 @@ type SouinBaseHandler struct { DefaultMatchedUrl configurationtypes.URL context *context.Context bufPool *sync.Pool + storersLen int } type upsreamError struct{} @@ -174,13 +176,35 @@ func (s *SouinBaseHandler) Upstream( if err == nil { variedHeaders := rfc.HeaderAllCommaSepValues(res.Header) cachedKey += rfc.GetVariedCacheKey(rq, variedHeaders) - if s.Storer.Set(cachedKey, response, currentMatchedURL, ma) == nil { + s.Configuration.GetLogger().Sugar().Debugf("Store the response %+v with duration %v", res, ma) + + var wg sync.WaitGroup + mu := sync.Mutex{} + fails := []string{} + for _, storer := range s.Storers { + wg.Add(1) + go func(currentStorer storage.Storer) { + defer wg.Done() + if currentStorer.Set(cachedKey, response, currentMatchedURL, ma) == nil { + s.Configuration.GetLogger().Sugar().Debugf("Stored the key %s in the %s provider", cachedKey, currentStorer.Name()) + } else { + mu.Lock() + fails = append(fails, fmt.Sprintf("; detail=%s-INSERTION-ERROR", currentStorer.Name())) + mu.Unlock() + } + }(storer) + } + + wg.Wait() + if len(fails) < s.storersLen { go func(rs http.Response, key string) { _ = s.SurrogateKeyStorer.Store(&rs, key) }(res, cachedKey) status += "; stored" - } else { - status += "; detail=STORAGE-INSERTION-ERROR" + } + + if len(fails) > 0 { + status += strings.Join(fails, "") } } } else { @@ -241,7 +265,14 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n customWriter := NewCustomWriter(rq, rw, bufPool) if !requestCc.NoCache { validator := rfc.ParseRequest(rq) - response := s.Storer.Prefix(cachedKey, rq, validator) + var response *http.Response + for _, currentStorer := range s.Storers { + response = currentStorer.Prefix(cachedKey, rq, validator) + if response != nil { + s.Configuration.GetLogger().Sugar().Debugf("Found response in the %s storage", currentStorer.Name()) + break + } + } if response != nil && rfc.ValidateCacheControl(response, requestCc) { rfc.SetCacheStatusHeader(response) @@ -254,7 +285,12 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n return nil } } else if response == nil && (requestCc.MaxStaleSet || requestCc.MaxStale > -1) { - response := s.Storer.Prefix(storage.StalePrefix+cachedKey, rq, validator) + for _, currentStorer := range s.Storers { + response = currentStorer.Prefix(storage.StalePrefix+cachedKey, rq, validator) + if response != nil { + break + } + } if nil != response && rfc.ValidateCacheControl(response, requestCc) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) rfc.SetCacheStatusHeader(response) diff --git a/plugins/tyk/override/storage/abstractProvider.go b/plugins/tyk/override/storage/abstractProvider.go index 4f1efc72f..30ebb1d24 100644 --- a/plugins/tyk/override/storage/abstractProvider.go +++ b/plugins/tyk/override/storage/abstractProvider.go @@ -24,13 +24,15 @@ type Storer interface { Delete(key string) DeleteMany(key string) Init() error + Name() string Reset() error } type StorerInstanciator func(configurationtypes.AbstractConfigurationInterface) (Storer, error) -func NewStorage(configuration configurationtypes.AbstractConfigurationInterface) (Storer, error) { - return CacheConnectionFactory(configuration) +func NewStorages(configuration configurationtypes.AbstractConfigurationInterface) ([]Storer, error) { + s, err := CacheConnectionFactory(configuration) + return []Storer{s}, err } func varyVoter(baseKey string, req *http.Request, currentKey string) bool { diff --git a/plugins/tyk/override/storage/cacheProvider.go b/plugins/tyk/override/storage/cacheProvider.go index c006acdaf..54c841007 100644 --- a/plugins/tyk/override/storage/cacheProvider.go +++ b/plugins/tyk/override/storage/cacheProvider.go @@ -25,6 +25,11 @@ func CacheConnectionFactory(c t.AbstractConfigurationInterface) (Storer, error) return &Cache{Cache: provider, stale: c.GetDefaultCache().GetStale()}, nil } +// Name returns the storer name +func (provider *Cache) Name() string { + return "CACHE" +} + // ListKeys method returns the list of existing keys func (provider *Cache) ListKeys() []string { items := provider.Items()