Add 'plugins/traefik/' from commit 'aef1f9e0dd205ea9cdea9e3ccf11900c5fe79b1f'

git-subtree-dir: plugins/traefik
git-subtree-mainline: 1a14070131
git-subtree-split: aef1f9e0dd
This commit is contained in:
Alexis Couvreur
2022-09-30 14:32:09 +00:00
86 changed files with 24035 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
package strategy
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type BlockingStrategy struct {
Requests []string
Name string
Next http.Handler
Timeout time.Duration
BlockDelay time.Duration
BlockCheckInterval time.Duration
}
type InternalServerError struct {
ServiceName string `json:"serviceName"`
Error string `json:"error"`
}
// ServeHTTP retrieve the service status
func (e *BlockingStrategy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
for start := time.Now(); time.Since(start) < e.BlockDelay; {
notReadyCount := 0
for _, request := range e.Requests {
log.Printf("Sending request: %s", request)
status, err := getServiceStatus(request)
log.Printf("Status: %s", status)
if err != nil {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(InternalServerError{ServiceName: e.Name, Error: err.Error()})
return
}
if status != "started" {
notReadyCount++
}
}
if notReadyCount == 0 {
// Services all started forward request
e.Next.ServeHTTP(rw, req)
return
}
time.Sleep(e.BlockCheckInterval)
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(rw).Encode(InternalServerError{ServiceName: e.Name, Error: fmt.Sprintf("Service was unreachable within %s", e.BlockDelay)})
}

View File

@@ -0,0 +1,88 @@
package strategy
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSingleBlockingStrategy_ServeHTTP(t *testing.T) {
for _, test := range SingleServiceTestCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.onDemandServiceResponses[0].status)
fmt.Fprint(w, test.onDemandServiceResponses[0].body)
}))
defer mockServer.Close()
blockingStrategy := &BlockingStrategy{
Name: "whoami",
Requests: []string{mockServer.URL},
Next: next,
BlockDelay: 1 * time.Second,
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
blockingStrategy.ServeHTTP(recorder, req)
assert.Equal(t, test.expected.blocking, recorder.Code)
})
}
}
func TestMultipleBlockingStrategy_ServeHTTP(t *testing.T) {
for _, test := range MultipleServicesTestCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
urls := make([]string, len(test.onDemandServiceResponses))
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
for responseIndex, response := range test.onDemandServiceResponses {
response := response
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(response.status)
fmt.Fprint(w, response.body)
}))
defer mockServer.Close()
urls[responseIndex] = mockServer.URL
}
fmt.Println(urls)
blockingStrategy := &BlockingStrategy{
Name: "whoami",
Requests: urls,
Next: next,
BlockDelay: 1 * time.Second,
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
blockingStrategy.ServeHTTP(recorder, req)
assert.Equal(t, test.expected.blocking, recorder.Code)
})
}
}

View File

@@ -0,0 +1,62 @@
package strategy
import (
"log"
"net/http"
"time"
"github.com/acouvreur/traefik-ondemand-plugin/pkg/pages"
)
type DynamicStrategy struct {
Requests []string
Name string
Next http.Handler
Timeout time.Duration
DisplayName string
LoadingPage string
ErrorPage string
}
// ServeHTTP retrieve the service status
func (e *DynamicStrategy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
started := make([]bool, len(e.Requests))
displayName := e.Name
if len(e.DisplayName) > 0 {
displayName = e.DisplayName
}
notReadyCount := 0
for requestIndex, request := range e.Requests {
log.Printf("Sending request: %s", request)
status, err := getServiceStatus(request)
log.Printf("Status: %s", status)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, displayName, err.Error())))
return
}
if status == "started" {
started[requestIndex] = true
} else if status == "starting" {
started[requestIndex] = false
notReadyCount++
} else {
// Error
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, displayName, status)))
return
}
}
if notReadyCount == 0 {
// All services are ready, forward request
e.Next.ServeHTTP(rw, req)
} else {
// Services still starting, notify client
rw.WriteHeader(http.StatusAccepted)
rw.Write([]byte(pages.GetLoadingPage(e.LoadingPage, displayName, e.Timeout)))
}
}

View File

@@ -0,0 +1,77 @@
package strategy
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSingleDynamicStrategy_ServeHTTP(t *testing.T) {
for _, test := range SingleServiceTestCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, test.onDemandServiceResponses[0].body)
}))
defer mockServer.Close()
dynamicStrategy := &DynamicStrategy{
Name: "whoami",
Requests: []string{mockServer.URL},
Next: next,
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
dynamicStrategy.ServeHTTP(recorder, req)
assert.Equal(t, test.expected.dynamic, recorder.Code)
})
}
}
func TestMultipleDynamicStrategy_ServeHTTP(t *testing.T) {
for _, test := range MultipleServicesTestCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
urls := make([]string, len(test.onDemandServiceResponses))
for responseIndex, response := range test.onDemandServiceResponses {
response := response
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, response.body)
}))
defer mockServer.Close()
urls[responseIndex] = mockServer.URL
}
dynamicStrategy := &DynamicStrategy{
Name: "whoami",
Requests: urls,
Next: next,
}
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
dynamicStrategy.ServeHTTP(recorder, req)
assert.Equal(t, test.expected.dynamic, recorder.Code)
})
}
}

View File

@@ -0,0 +1,39 @@
package strategy
import (
"errors"
"io/ioutil"
"net/http"
"strings"
"time"
)
// Net client is a custom client to timeout after 2 seconds if the service is not ready
var netClient = &http.Client{
Timeout: time.Second * 2,
}
type Strategy interface {
ServeHTTP(rw http.ResponseWriter, req *http.Request)
}
func getServiceStatus(request string) (string, error) {
// This request wakes up the service if he's scaled to 0
resp, err := netClient.Get(request)
if err != nil {
return "error", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "parsing error", err
}
if resp.StatusCode >= 400 {
return "error from ondemand service", errors.New(string(body))
}
return strings.TrimSuffix(string(body), "\n"), nil
}

View File

@@ -0,0 +1,135 @@
package strategy
type OnDemandServiceResponses struct {
body string
status int
}
type ExpectedStatusForStrategy struct {
dynamic int
blocking int
}
type TestCase struct {
desc string
onDemandServiceResponses []OnDemandServiceResponses
expected ExpectedStatusForStrategy
}
var SingleServiceTestCases = []TestCase{
{
desc: "service is / keeps on starting",
onDemandServiceResponses: GenerateServicesResponses(1, "starting"),
expected: ExpectedStatusForStrategy{
dynamic: 202,
blocking: 503,
},
},
{
desc: "service is started",
onDemandServiceResponses: GenerateServicesResponses(1, "started"),
expected: ExpectedStatusForStrategy{
dynamic: 200,
blocking: 200,
},
},
{
desc: "ondemand service is in error",
onDemandServiceResponses: GenerateServicesResponses(1, "error"),
expected: ExpectedStatusForStrategy{
dynamic: 500,
blocking: 500,
},
},
}
func GenerateServicesResponses(count int, serviceBody string) []OnDemandServiceResponses {
responses := make([]OnDemandServiceResponses, count)
for i := 0; i < count; i++ {
if serviceBody == "starting" || serviceBody == "started" {
responses[i] = OnDemandServiceResponses{
body: serviceBody,
status: 200,
}
} else {
responses[i] = OnDemandServiceResponses{
body: serviceBody,
status: 503,
}
}
}
return responses
}
var MultipleServicesTestCases = []TestCase{
{
desc: "all services are starting",
onDemandServiceResponses: GenerateServicesResponses(5, "starting"),
expected: ExpectedStatusForStrategy{
dynamic: 202,
blocking: 503,
},
},
{
desc: "one started others are starting",
onDemandServiceResponses: append(GenerateServicesResponses(1, "starting"), GenerateServicesResponses(4, "started")...),
expected: ExpectedStatusForStrategy{
dynamic: 202,
blocking: 503,
},
},
{
desc: "one starting others are started",
onDemandServiceResponses: append(GenerateServicesResponses(4, "starting"), GenerateServicesResponses(1, "started")...),
expected: ExpectedStatusForStrategy{
dynamic: 202,
blocking: 503,
},
},
{
desc: "one errored others are starting",
onDemandServiceResponses: append(
GenerateServicesResponses(2, "starting"),
append(
GenerateServicesResponses(1, "error"),
GenerateServicesResponses(2, "starting")...,
)...,
),
expected: ExpectedStatusForStrategy{
dynamic: 500,
blocking: 500,
},
},
{
desc: "one errored others are started",
onDemandServiceResponses: append(
GenerateServicesResponses(1, "error"),
GenerateServicesResponses(4, "started")...,
),
expected: ExpectedStatusForStrategy{
dynamic: 500,
blocking: 500,
},
},
{
desc: "one errored others are mix of starting / started",
onDemandServiceResponses: append(
GenerateServicesResponses(2, "started"),
append(
GenerateServicesResponses(1, "error"),
GenerateServicesResponses(2, "starting")...,
)...,
),
expected: ExpectedStatusForStrategy{
dynamic: 500,
blocking: 500,
},
},
{
desc: "all are started",
onDemandServiceResponses: GenerateServicesResponses(5, "started"),
expected: ExpectedStatusForStrategy{
dynamic: 200,
blocking: 200,
},
},
}