mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-24 06:28:21 +01:00
feat(plugins): add traefik plugin
This commit is contained in:
15
.traefik.yml
15
.traefik.yml
@@ -7,7 +7,14 @@ import: github.com/acouvreur/sablier/plugins/traefik
|
|||||||
summary: "Start your containers on demand, shut them down automatically when there's no activity. Docker, Docker Swarm Mode and Kubernetes compatible."
|
summary: "Start your containers on demand, shut them down automatically when there's no activity. Docker, Docker Swarm Mode and Kubernetes compatible."
|
||||||
|
|
||||||
testData:
|
testData:
|
||||||
serviceUrl: http://sablier:10000
|
sablierUrl: http://sablier:10000
|
||||||
name: whoami
|
names: whoami,nginx # comma separated names
|
||||||
timeout: 1m
|
sessionDuration: 1m
|
||||||
|
# Dynamic strategy, provides the waiting webui
|
||||||
|
dynamic:
|
||||||
|
displayName: My Title
|
||||||
|
theme: hacker-terminal
|
||||||
|
# Blocking strategy, waits until services are up and running
|
||||||
|
# but will not wait more than `timeout`
|
||||||
|
blocking:
|
||||||
|
timeout: 1m
|
||||||
@@ -3,7 +3,7 @@ package models
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type BlockingRequest struct {
|
type BlockingRequest struct {
|
||||||
Names []string
|
Names []string `form:"names" binding:"required"`
|
||||||
SessionDuration time.Duration
|
SessionDuration time.Duration `form:"session_duration" binding:"required"`
|
||||||
Timeout time.Duration
|
Timeout time.Duration `form:"timeout"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
type DynamicRequest struct {
|
type DynamicRequest struct {
|
||||||
Names []string `form:"names" binding:"required"`
|
Names []string `form:"names" binding:"required"`
|
||||||
DisplayName string `form:"display-name" binding:"required"`
|
DisplayName string `form:"display_name"`
|
||||||
Theme string `form:"theme" binding:"required"`
|
Theme string `form:"theme"`
|
||||||
SessionDuration time.Duration `form:"session-duration" binding:"required"`
|
SessionDuration time.Duration `form:"session_duration" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ func (s *ServeStrategy) ServeDynamic(c *gin.Context) {
|
|||||||
sessionState := s.SessionsManager.RequestSession(request.Names, request.SessionDuration)
|
sessionState := s.SessionsManager.RequestSession(request.Names, request.SessionDuration)
|
||||||
|
|
||||||
if sessionState.IsReady() {
|
if sessionState.IsReady() {
|
||||||
// All requests are fulfilled, redirect to
|
c.Header("X-Sablier-Session-Status", "ready")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "origin")
|
} else {
|
||||||
return
|
c.Header("X-Sablier-Session-Status", "not-ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOptions := pages.RenderOptions{
|
renderOptions := pages.RenderOptions{
|
||||||
@@ -67,9 +67,9 @@ func (s *ServeStrategy) ServeBlocking(c *gin.Context) {
|
|||||||
sessionState := s.SessionsManager.RequestReadySession(request.Names, request.SessionDuration, request.Timeout)
|
sessionState := s.SessionsManager.RequestReadySession(request.Names, request.SessionDuration, request.Timeout)
|
||||||
|
|
||||||
if sessionState.IsReady() {
|
if sessionState.IsReady() {
|
||||||
// All requests are fulfilled, redirect to
|
c.Header("X-Sablier-Session-Status", "ready")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "origin")
|
} else {
|
||||||
return
|
c.Header("X-Sablier-Session-Status", "not-ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ func TestServeStrategy_ServeDynamic(t *testing.T) {
|
|||||||
session sessions.SessionState
|
session sessions.SessionState
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
arg arg
|
arg arg
|
||||||
expectedCode int
|
expectedHeaderKey string
|
||||||
|
expectedHeaderValue string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "return HTML Theme",
|
name: "return HTML Theme",
|
||||||
@@ -62,7 +63,8 @@ func TestServeStrategy_ServeDynamic(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCode: http.StatusOK,
|
expectedHeaderKey: "X-Sablier-Session-Status",
|
||||||
|
expectedHeaderValue: "not-ready",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "temporary redirect when session is ready",
|
name: "temporary redirect when session is ready",
|
||||||
@@ -79,7 +81,8 @@ func TestServeStrategy_ServeDynamic(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCode: http.StatusTemporaryRedirect,
|
expectedHeaderKey: "X-Sablier-Session-Status",
|
||||||
|
expectedHeaderValue: "ready",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -99,7 +102,7 @@ func TestServeStrategy_ServeDynamic(t *testing.T) {
|
|||||||
res := recorder.Result()
|
res := recorder.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
assert.Equal(t, c.Writer.Status(), tt.expectedCode)
|
assert.Equal(t, c.Writer.Header().Get(tt.expectedHeaderKey), tt.expectedHeaderValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (s *SessionsManager) requestSessionInstance(name string, duration time.Dura
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionsManager) RequestReadySession(names []string, duration time.Duration, timeout time.Duration) *SessionState {
|
func (s *SessionsManager) RequestReadySession(names []string, duration time.Duration, timeout time.Duration) *SessionState {
|
||||||
return nil
|
return s.RequestSession(names, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionsManager) ExpiresAfter(instance *instance.State, duration time.Duration) {
|
func (s *SessionsManager) ExpiresAfter(instance *instance.State, duration time.Duration) {
|
||||||
|
|||||||
122
plugins/traefik/config.go
Normal file
122
plugins/traefik/config.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package traefik
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DynamicConfiguration struct {
|
||||||
|
DisplayName string `yaml:"displayname"`
|
||||||
|
Theme string `yaml:"theme"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockingConfiguration struct {
|
||||||
|
Timeout string `yaml:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SablierURL string `yaml:"sablierUrl"`
|
||||||
|
Names string `yaml:"names"`
|
||||||
|
SessionDuration string `yaml:"sessionDuration"`
|
||||||
|
splittedNames []string
|
||||||
|
Dynamic *DynamicConfiguration `yaml:"dynamic"`
|
||||||
|
Blocking *BlockingConfiguration `yaml:"blocking"`
|
||||||
|
Next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "",
|
||||||
|
SessionDuration: "",
|
||||||
|
splittedNames: []string{},
|
||||||
|
Dynamic: nil,
|
||||||
|
Blocking: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) BuildRequest() (*http.Request, error) {
|
||||||
|
|
||||||
|
if len(c.SablierURL) == 0 {
|
||||||
|
return nil, fmt.Errorf("sablierURL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
names := strings.Split(c.Names, ",")
|
||||||
|
for i := range names {
|
||||||
|
names[i] = strings.TrimSpace(names[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
c.splittedNames = names
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("you must specify at least one name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Dynamic != nil && c.Blocking != nil {
|
||||||
|
return nil, fmt.Errorf("only supply one strategy: dynamic or blocking")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Dynamic != nil {
|
||||||
|
return c.buildDynamicRequest()
|
||||||
|
} else if c.Blocking != nil {
|
||||||
|
return c.buildBlockingRequest()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no strategy configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) buildDynamicRequest() (*http.Request, error) {
|
||||||
|
if c.Dynamic == nil {
|
||||||
|
return nil, fmt.Errorf("dynamic config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("%s/api/strategies/dynamic", c.SablierURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := request.URL.Query()
|
||||||
|
|
||||||
|
q.Add("session_duration", c.SessionDuration)
|
||||||
|
for _, name := range c.splittedNames {
|
||||||
|
q.Add("names", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Dynamic.DisplayName != "" {
|
||||||
|
q.Add("display_name", c.Dynamic.DisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Dynamic.Theme != "" {
|
||||||
|
q.Add("theme", c.Dynamic.Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) buildBlockingRequest() (*http.Request, error) {
|
||||||
|
if c.Blocking == nil {
|
||||||
|
return nil, fmt.Errorf("blocking config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("%s/api/strategies/blocking", c.SablierURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := request.URL.Query()
|
||||||
|
|
||||||
|
q.Add("session_duration", c.SessionDuration)
|
||||||
|
for _, name := range c.splittedNames {
|
||||||
|
q.Add("names", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Blocking.Timeout != "" {
|
||||||
|
q.Add("timeout", c.Blocking.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
129
plugins/traefik/config_test.go
Normal file
129
plugins/traefik/config_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package traefik_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/plugins/traefik"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_BuildRequest(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SablierURL string
|
||||||
|
Names string
|
||||||
|
SessionDuration string
|
||||||
|
Dynamic *traefik.DynamicConfiguration
|
||||||
|
Blocking *traefik.BlockingConfiguration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want *http.Request
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic session with default values",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Dynamic: &traefik.DynamicConfiguration{},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?names=nginx&names=apache&session_duration=1m", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic session with theme values",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Dynamic: &traefik.DynamicConfiguration{
|
||||||
|
Theme: "hacker-terminal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?names=nginx&names=apache&session_duration=1m&theme=hacker-terminal", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic session with theme and display name values",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Dynamic: &traefik.DynamicConfiguration{
|
||||||
|
Theme: "hacker-terminal",
|
||||||
|
DisplayName: "Hello World!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?display_name=Hello+World%21&names=nginx&names=apache&session_duration=1m&theme=hacker-terminal", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blocking session with default values",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Blocking: &traefik.BlockingConfiguration{},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/blocking?names=nginx&names=apache&session_duration=1m", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blocking session with timeout value",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Blocking: &traefik.BlockingConfiguration{
|
||||||
|
Timeout: "5m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/blocking?names=nginx&names=apache&session_duration=1m&timeout=5m", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both strategies defined",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Names: "nginx , apache",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Dynamic: &traefik.DynamicConfiguration{},
|
||||||
|
Blocking: &traefik.BlockingConfiguration{},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &traefik.Config{
|
||||||
|
SablierURL: tt.fields.SablierURL,
|
||||||
|
Names: tt.fields.Names,
|
||||||
|
SessionDuration: tt.fields.SessionDuration,
|
||||||
|
Dynamic: tt.fields.Dynamic,
|
||||||
|
Blocking: tt.fields.Blocking,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := c.BuildRequest()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Config.BuildRequest() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Config.BuildRequest() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequest(method string, url string, body io.Reader) *http.Request {
|
||||||
|
request, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
51
plugins/traefik/main.go
Normal file
51
plugins/traefik/main.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package traefik
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SablierMiddleware struct {
|
||||||
|
client *http.Client
|
||||||
|
request *http.Request
|
||||||
|
Next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// New function creates the configuration
|
||||||
|
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
||||||
|
req, err := config.BuildRequest()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SablierMiddleware{
|
||||||
|
request: req,
|
||||||
|
client: &http.Client{},
|
||||||
|
Next: config.Next,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SablierMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
sablierRequest := sm.request.Clone(context.TODO())
|
||||||
|
|
||||||
|
resp, err := sm.client.Do(sablierRequest)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.Header.Get("X-Sablier-Session-Status") == "ready" {
|
||||||
|
sm.Next.ServeHTTP(rw, req)
|
||||||
|
} else {
|
||||||
|
forward(resp, rw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forward(resp *http.Response, rw http.ResponseWriter) {
|
||||||
|
rw.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
||||||
|
rw.Header().Set("Content-Length", resp.Header.Get("Content-Length"))
|
||||||
|
io.Copy(rw, resp.Body)
|
||||||
|
}
|
||||||
97
plugins/traefik/main_test.go
Normal file
97
plugins/traefik/main_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package traefik
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSablierMiddleware_ServeHTTP(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Next http.Handler
|
||||||
|
Config *Config
|
||||||
|
}
|
||||||
|
type sablier struct {
|
||||||
|
headers map[string]string
|
||||||
|
body string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
sablier sablier
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sablier service is ready",
|
||||||
|
sablier: sablier{
|
||||||
|
headers: map[string]string{
|
||||||
|
"X-Sablier-Session-Status": "ready",
|
||||||
|
},
|
||||||
|
body: "response from sablier",
|
||||||
|
},
|
||||||
|
fields: fields{
|
||||||
|
Config: &Config{
|
||||||
|
Dynamic: &DynamicConfiguration{},
|
||||||
|
Next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, "response from service")
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "response from service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sablier service is not ready",
|
||||||
|
sablier: sablier{
|
||||||
|
headers: map[string]string{
|
||||||
|
"X-Sablier-Session-Status": "not-ready",
|
||||||
|
},
|
||||||
|
body: "response from sablier",
|
||||||
|
},
|
||||||
|
fields: fields{
|
||||||
|
Config: &Config{
|
||||||
|
Dynamic: &DynamicConfiguration{},
|
||||||
|
Next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, "response from service")
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "response from sablier",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sablierMockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for key, value := range tt.sablier.headers {
|
||||||
|
w.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
w.Write([]byte(tt.sablier.body))
|
||||||
|
}))
|
||||||
|
defer sablierMockServer.Close()
|
||||||
|
|
||||||
|
tt.fields.Config.SablierURL = sablierMockServer.URL
|
||||||
|
|
||||||
|
sm, err := New(context.Background(), tt.fields.Next, tt.fields.Config, "middleware")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/my-nginx", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
sm.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected error to be nil got %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != tt.expected {
|
||||||
|
t.Errorf("expected %s got %v", tt.expected, string(data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package traefik
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/acouvreur/sablier/plugins/traefik/pkg/strategy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config the plugin configuration
|
|
||||||
type Config struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Names []string `yaml:"names"`
|
|
||||||
ServiceUrl string `yaml:"serviceurl"`
|
|
||||||
Timeout string `yaml:"timeout"`
|
|
||||||
ErrorPage string `yaml:"errorpage"`
|
|
||||||
LoadingPage string `yaml:"loadingpage"`
|
|
||||||
WaitUi bool `yaml:"waitui"`
|
|
||||||
DisplayName string `yaml:"displayname"`
|
|
||||||
BlockDelay string `yaml:"blockdelay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateConfig creates a config with its default values
|
|
||||||
func CreateConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
Timeout: "1m",
|
|
||||||
WaitUi: true,
|
|
||||||
BlockDelay: "1m",
|
|
||||||
DisplayName: "",
|
|
||||||
ErrorPage: "",
|
|
||||||
LoadingPage: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ondemand holds the request for the on demand service
|
|
||||||
type Ondemand struct {
|
|
||||||
strategy strategy.Strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRequest(url string, name string, timeout time.Duration) (string, error) {
|
|
||||||
request := fmt.Sprintf("%s?name=%s&timeout=%s", url, name, timeout.String())
|
|
||||||
return request, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New function creates the configuration
|
|
||||||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
|
||||||
if len(config.ServiceUrl) == 0 {
|
|
||||||
return nil, fmt.Errorf("serviceurl cannot be null")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(config.Name) != 0 && len(config.Names) != 0 {
|
|
||||||
return nil, fmt.Errorf("both name and names cannot be used simultaneously")
|
|
||||||
}
|
|
||||||
var serviceNames []string
|
|
||||||
|
|
||||||
if len(config.Name) != 0 {
|
|
||||||
serviceNames = append(serviceNames, config.Name)
|
|
||||||
} else if len(config.Names) != 0 {
|
|
||||||
serviceNames = config.Names
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("both name and names cannot be null")
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout, err := time.ParseDuration(config.Timeout)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var requests []string
|
|
||||||
|
|
||||||
for _, serviceName := range serviceNames {
|
|
||||||
request, err := buildRequest(config.ServiceUrl, serviceName, timeout)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error while building request for %s", serviceName)
|
|
||||||
}
|
|
||||||
requests = append(requests, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
strategy, err := config.getServeStrategy(requests, name, next, timeout)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Ondemand{
|
|
||||||
strategy: strategy,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *Config) getServeStrategy(requests []string, name string, next http.Handler, timeout time.Duration) (strategy.Strategy, error) {
|
|
||||||
if config.WaitUi {
|
|
||||||
return &strategy.DynamicStrategy{
|
|
||||||
Requests: requests,
|
|
||||||
Name: name,
|
|
||||||
Next: next,
|
|
||||||
Timeout: timeout,
|
|
||||||
DisplayName: config.DisplayName,
|
|
||||||
ErrorPage: config.ErrorPage,
|
|
||||||
LoadingPage: config.LoadingPage,
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
|
|
||||||
blockDelay, err := time.ParseDuration(config.BlockDelay)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &strategy.BlockingStrategy{
|
|
||||||
Requests: requests,
|
|
||||||
Name: name,
|
|
||||||
Next: next,
|
|
||||||
Timeout: timeout,
|
|
||||||
BlockDelay: blockDelay,
|
|
||||||
BlockCheckInterval: 1 * time.Second,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP retrieve the service status
|
|
||||||
func (e *Ondemand) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
e.strategy.ServeHTTP(rw, req)
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package traefik
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewOndemand(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
config *Config
|
|
||||||
expectedError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Invalid Config (no name)",
|
|
||||||
config: &Config{
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Invalid Config (empty names)",
|
|
||||||
config: &Config{
|
|
||||||
Names: []string{},
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Invalid Config (empty serviceUrl)",
|
|
||||||
config: &Config{
|
|
||||||
Name: "whoami",
|
|
||||||
ServiceUrl: "",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Invalid Config (name and names used simultaneously)",
|
|
||||||
config: &Config{
|
|
||||||
Names: []string{
|
|
||||||
"whoami-1", "whoami-2",
|
|
||||||
},
|
|
||||||
Name: "whoami",
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
WaitUi: true,
|
|
||||||
BlockDelay: "1m",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "valid Dynamic Config",
|
|
||||||
config: &Config{
|
|
||||||
Name: "whoami",
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
WaitUi: true,
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "valid Blocking Config",
|
|
||||||
config: &Config{
|
|
||||||
Name: "whoami",
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
WaitUi: false,
|
|
||||||
BlockDelay: "1m",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "valid Dynamic Multiple Config",
|
|
||||||
config: &Config{
|
|
||||||
Names: []string{
|
|
||||||
"whoami-1", "whoami-2",
|
|
||||||
},
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
WaitUi: false,
|
|
||||||
BlockDelay: "1m",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "valid Blocking Multiple Config",
|
|
||||||
config: &Config{
|
|
||||||
Names: []string{
|
|
||||||
"whoami-1", "whoami-2",
|
|
||||||
},
|
|
||||||
ServiceUrl: "http://ondemand:1000",
|
|
||||||
WaitUi: true,
|
|
||||||
BlockDelay: "1m",
|
|
||||||
Timeout: "1m",
|
|
||||||
},
|
|
||||||
expectedError: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
ondemand, err := New(context.Background(), next, test.config, "traefikTest")
|
|
||||||
|
|
||||||
if test.expectedError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, ondemand)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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)})
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package strategy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/acouvreur/sablier/plugins/traefik/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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package strategy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"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 SablierResponse struct {
|
|
||||||
State string `json:"state"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body)
|
|
||||||
var response SablierResponse
|
|
||||||
err = decoder.Decode(&response)
|
|
||||||
if err != nil {
|
|
||||||
return "error from ondemand service", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return "error from ondemand service", errors.New(response.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.State, nil
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package strategy
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
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 {
|
|
||||||
body, err := json.Marshal(SablierResponse{State: serviceBody, Error: "ko"})
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
responses := make([]OnDemandServiceResponses, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
if serviceBody == "starting" || serviceBody == "started" {
|
|
||||||
responses[i] = OnDemandServiceResponses{
|
|
||||||
body: string(body),
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
responses[i] = OnDemandServiceResponses{
|
|
||||||
body: string(body),
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user