mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-30 17:47:26 +01:00
feat: add blocking request (#12)
* feat: add blocking strategy * docs: add examples for blocking strategy * ci: run go tests recursively * perf: wait for BlockCheckInterval for each request * fix: use camel case instead of snake case for yaml config * docs: add loading and error page customization as a feature * fix: use errorPage * fix: return json instead of html page * set json key * update development config * docs: add comment about custom loading pages * ci: add beta release
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -22,4 +22,4 @@ jobs:
|
||||
run: go build -v .
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
run: go test -v ./...
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -22,4 +22,4 @@ jobs:
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release -b main -p "@semantic-release/commit-analyzer" -p "@semantic-release/release-notes-generator" -p "@semantic-release/github"
|
||||
run: npx semantic-release -b main -b beta
|
||||
50
README.md
50
README.md
@@ -1,7 +1,5 @@
|
||||
|
||||
# Traefik Ondemand Plugin
|
||||
|
||||
|
||||
Traefik middleware to start containers on demand.
|
||||
|
||||

|
||||
@@ -17,15 +15,35 @@ Traefik middleware to start containers on demand.
|
||||
- Start your container/service on the first request
|
||||
- Automatic **scale to zero** after configured timeout upon last request the service received
|
||||
- Dynamic loading page (cloudflare or grafana cloud style)
|
||||
- Customize dynamic and loading pages
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
### Plugin configuration
|
||||
|
||||
#### Strategies
|
||||
|
||||
**Dynamic Strategy**
|
||||
|
||||
_Serve an HTML page that self reload._
|
||||
|
||||
```yml
|
||||
testData:
|
||||
serviceUrl: http://ondemand:10000
|
||||
name: TRAEFIK_HACKATHON_whoami
|
||||
timeout: 1m
|
||||
waitUi: true
|
||||
```
|
||||
|
||||
**Blocking Strategy**
|
||||
|
||||
_Responds as soon as the service is up with a maximum waiting time of `blockingDelay`_
|
||||
|
||||
#### Custom loading/error pages
|
||||
|
||||
The `loadingpage` and `errorpage` keys in the plugin configuration can be used to override the default loading and error pages.
|
||||
The `loadingpage` and `errorpage` keys in the plugin configuration can be used to override the default loading and error pages.
|
||||
|
||||
The value should be a path where a template that can be parsed by Go's [html/template](https://pkg.go.dev/html/template) package can be found in the Traefik container.
|
||||
|
||||
@@ -33,23 +51,30 @@ An example of both a loading page and an error page template can be found in the
|
||||
|
||||
The plugin will default to the built-in loading and error pages if these fields are omitted.
|
||||
|
||||
You must include `<meta http-equiv="refresh" content="5" />` inside your html page to get auto refresh.
|
||||
|
||||
**Example Configuration**
|
||||
|
||||
```yml
|
||||
testData:
|
||||
serviceUrl: http://ondemand:10000
|
||||
name: TRAEFIK_HACKATHON_whoami
|
||||
timeout: 1m
|
||||
waitUi: false
|
||||
blockingDelay: 1m
|
||||
loadingpage: /opt/on-demand/loading.html
|
||||
errorpage: /opt/on-demand/error.html
|
||||
```
|
||||
|
||||
| Parameter | Type | Example | Description |
|
||||
| ------------ | --------------- | -------------------------- | ----------------------------------------------------------------------- |
|
||||
| `serviceUrl` | `string` | `http://ondemand:10000` | The docker container name, or the swarm service name |
|
||||
| `name` | `string` | `TRAEFIK_HACKATHON_whoami` | The container/service to be stopped (docker ps | docker service ls) |
|
||||
| `timeout` | `time.Duration` | `1m30s` | The duration after which the container/service will be scaled down to 0 |
|
||||
| `loadingpage`| `string` | `/opt/on-demand/loading.html` | The path in the traefik container for the loading page template |
|
||||
| `errorpage` | `string` | `/opt/on-demand/error.html` | The path in the traefik container for the error page template |
|
||||
| Parameter | Type | Example | Description |
|
||||
| --------------- | --------------- | ----------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `serviceUrl` | `string` | `http://ondemand:10000` | The docker container name, or the swarm service name |
|
||||
| `name` | `string` | `TRAEFIK_HACKATHON_whoami` | The container/service to be stopped (docker ps docker service ls) |
|
||||
| `timeout` | `time.Duration` | `1m30s` | The duration after which the container/service will be scaled down to 0 |
|
||||
| `waitUi` | `bool` | `true` | Serves a self-refreshing html page when the service is scaled down to 0 |
|
||||
| `blockingDelay` | `time.Duration` | `1m30s` | When `waitUi` is `false`, wait for the service to be scaled up before `blockingDelay` |
|
||||
| `loadingpage` | `string` | `/opt/on-demand/loading.html` | The path in the traefik container for the loading page template |
|
||||
| `errorpage` | `string` | `/opt/on-demand/error.html` | The path in the traefik container for the error page template |
|
||||
|
||||
### Traefik-Ondemand-Service
|
||||
|
||||
@@ -66,6 +91,11 @@ The docker library that interacts with the docker deamon uses `unsafe` which mus
|
||||
- [Multiple Containers](./examples/multiple_containers/)
|
||||
- [Kubernetes](./examples/kubernetes/)
|
||||
|
||||
## Development
|
||||
|
||||
`export TRAEFIK_PILOT_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
||||
`docker stack deploy -c docker-compose.yml TRAEFIK_HACKATHON`
|
||||
|
||||
## Authors
|
||||
|
||||
[Alexis Couvreur](https://www.linkedin.com/in/alexis-couvreur/) (left)
|
||||
|
||||
23
config.yml
23
config.yml
@@ -1,23 +0,0 @@
|
||||
http:
|
||||
middlewares:
|
||||
ondemand:
|
||||
plugin:
|
||||
ondemand:
|
||||
serviceurl: http://ondemand:10000
|
||||
name: TRAEFIK_HACKATHON_whoami
|
||||
timeout: 1m
|
||||
|
||||
services:
|
||||
whoami:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://whoami:80"
|
||||
|
||||
routers:
|
||||
whoami:
|
||||
rule: PathPrefix(`/whoami`)
|
||||
entryPoints:
|
||||
- "http"
|
||||
middlewares:
|
||||
- ondemand
|
||||
service: "whoami"
|
||||
@@ -10,7 +10,6 @@ services:
|
||||
volumes:
|
||||
- './traefik_dev.yml:/etc/traefik/traefik-template.yml'
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
- './config.yml:/etc/traefik/config.yml'
|
||||
- '.:/plugins-local/src/github.com/acouvreur/traefik-ondemand-plugin'
|
||||
environment:
|
||||
- TRAEFIK_PILOT_TOKEN
|
||||
@@ -20,11 +19,47 @@ services:
|
||||
- traefik.http.services.traefik.loadbalancer.server.port=8080
|
||||
|
||||
ondemand:
|
||||
image: ghcr.io/acouvreur/traefik-ondemand-service:1
|
||||
image: ghcr.io/acouvreur/traefik-ondemand-service:1.7
|
||||
command:
|
||||
- --swarmMode=true
|
||||
volumes:
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
deploy:
|
||||
replicas: 0
|
||||
replicas: 0
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# If you do not use the swarm load balancer, traefik will evict the service from its pool
|
||||
# as soon as the service is 0/0. If you do not set that, fallback to dynamic-config.yml file usage.
|
||||
- traefik.docker.lbswarm=true
|
||||
- traefik.http.middlewares.ondemand_whoami.plugin.traefik-ondemand-plugin.name=TRAEFIK_HACKATHON_whoami
|
||||
- traefik.http.middlewares.ondemand_whoami.plugin.traefik-ondemand-plugin.serviceurl=http://ondemand:10000
|
||||
- traefik.http.middlewares.ondemand_whoami.plugin.traefik-ondemand-plugin.timeout=1m
|
||||
- traefik.http.routers.whoami.middlewares=ondemand_whoami@docker
|
||||
- traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)
|
||||
- traefik.http.services.whoami.loadbalancer.server.port=80
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
healthcheck:
|
||||
test: "true"
|
||||
interval: 1m30s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
deploy:
|
||||
replicas: 0
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# If you do not use the swarm load balancer, traefik will evict the service from its pool
|
||||
# as soon as the service is 0/0. If you do not set that, fallback to dynamic-config.yml file usage.
|
||||
- traefik.docker.lbswarm=true
|
||||
- traefik.http.middlewares.ondemand_nginx.plugin.traefik-ondemand-plugin.name=TRAEFIK_HACKATHON_nginx
|
||||
- traefik.http.middlewares.ondemand_nginx.plugin.traefik-ondemand-plugin.serviceurl=http://ondemand:10000
|
||||
- traefik.http.middlewares.ondemand_nginx.plugin.traefik-ondemand-plugin.timeout=5m
|
||||
- traefik.http.middlewares.ondemand_nginx.plugin.traefik-ondemand-plugin.waitUi=false
|
||||
- traefik.http.routers.nginx.middlewares=ondemand_nginx@docker
|
||||
- traefik.http.routers.nginx.rule=PathPrefix(`/nginx`)
|
||||
- traefik.http.services.nginx.loadbalancer.server.port=80
|
||||
108
ondemand.go
108
ondemand.go
@@ -3,20 +3,12 @@ package traefik_ondemand_plugin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acouvreur/traefik-ondemand-plugin/pkg/pages"
|
||||
"github.com/acouvreur/traefik-ondemand-plugin/pkg/strategy"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Config the plugin configuration
|
||||
type Config struct {
|
||||
Name string `yaml:"name"`
|
||||
@@ -24,23 +16,24 @@ type Config struct {
|
||||
Timeout string `yaml:"timeout"`
|
||||
ErrorPage string `yaml:"errorpage"`
|
||||
LoadingPage string `yaml:"loadingpage"`
|
||||
WaitUi bool `yaml:"waitUi"`
|
||||
BlockDelay string `yaml:"blockDelay"`
|
||||
}
|
||||
|
||||
// CreateConfig creates a config with its default values
|
||||
func CreateConfig() *Config {
|
||||
return &Config{
|
||||
Timeout: "1m",
|
||||
Timeout: "1m",
|
||||
WaitUi: true,
|
||||
BlockDelay: "1m",
|
||||
ErrorPage: "",
|
||||
LoadingPage: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Ondemand holds the request for the on demand service
|
||||
type Ondemand struct {
|
||||
request string
|
||||
name string
|
||||
next http.Handler
|
||||
timeout time.Duration
|
||||
errorpage string
|
||||
loadingpage string
|
||||
strategy strategy.Strategy
|
||||
}
|
||||
|
||||
func buildRequest(url string, name string, timeout time.Duration) (string, error) {
|
||||
@@ -70,56 +63,47 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
return nil, fmt.Errorf("error while building request")
|
||||
}
|
||||
|
||||
strategy, err := config.getServeStrategy(request, name, next, timeout)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Ondemand{
|
||||
next: next,
|
||||
name: config.Name,
|
||||
request: request,
|
||||
timeout: timeout,
|
||||
errorpage: config.ErrorPage,
|
||||
loadingpage: config.LoadingPage,
|
||||
strategy: strategy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (config *Config) getServeStrategy(request string, name string, next http.Handler, timeout time.Duration) (strategy.Strategy, error) {
|
||||
if config.WaitUi {
|
||||
return &strategy.DynamicStrategy{
|
||||
Request: request,
|
||||
Name: name,
|
||||
Next: next,
|
||||
Timeout: timeout,
|
||||
ErrorPage: config.ErrorPage,
|
||||
LoadingPage: config.LoadingPage,
|
||||
}, nil
|
||||
} else {
|
||||
|
||||
blockDelay, err := time.ParseDuration(config.BlockDelay)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &strategy.BlockingStrategy{
|
||||
Request: request,
|
||||
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) {
|
||||
|
||||
log.Printf("Sending request: %s", e.request)
|
||||
status, err := getServiceStatus(e.request)
|
||||
log.Printf("Status: %s", status)
|
||||
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.errorpage, e.name, err.Error())))
|
||||
}
|
||||
|
||||
if status == "started" {
|
||||
// Service started forward request
|
||||
e.next.ServeHTTP(rw, req)
|
||||
|
||||
} else if status == "starting" {
|
||||
// Service starting, notify client
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
rw.Write([]byte(pages.GetLoadingPage(e.loadingpage, e.name, e.timeout)))
|
||||
} else {
|
||||
// Error
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.errorpage, e.name, status)))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(string(body), "\n"), nil
|
||||
e.strategy.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package traefik_ondemand_plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -26,10 +24,22 @@ func TestNewOndemand(t *testing.T) {
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Config",
|
||||
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,
|
||||
@@ -53,58 +63,3 @@ func TestNewOndemand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOndemand_ServeHTTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
status string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "service is starting",
|
||||
status: "starting",
|
||||
expected: 202,
|
||||
},
|
||||
{
|
||||
desc: "service is started",
|
||||
status: "started",
|
||||
expected: 200,
|
||||
},
|
||||
{
|
||||
desc: "ondemand service is in error",
|
||||
status: "error",
|
||||
expected: 500,
|
||||
},
|
||||
}
|
||||
|
||||
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) {})
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, test.status)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
config := &Config{
|
||||
Name: "whoami",
|
||||
ServiceUrl: mockServer.URL,
|
||||
Timeout: "1m",
|
||||
}
|
||||
ondemand, err := New(context.Background(), next, config, "traefikTest")
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
|
||||
|
||||
ondemand.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expected, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func GetErrorPage(template_path string, name string, e string) string {
|
||||
if template_path != "" {
|
||||
tpl, err = template.New(path.Base(template_path)).ParseFiles(template_path)
|
||||
} else {
|
||||
tpl, err = template.New("loading").Parse(loadingPage)
|
||||
tpl, err = template.New("loading").Parse(errorPage)
|
||||
}
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
|
||||
51
pkg/strategy/blocking_strategy.go
Normal file
51
pkg/strategy/blocking_strategy.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BlockingStrategy struct {
|
||||
Request 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; {
|
||||
log.Printf("Sending request: %s", e.Request)
|
||||
status, err := getServiceStatus(e.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" {
|
||||
// Service 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)})
|
||||
}
|
||||
73
pkg/strategy/blocking_strategy_test.go
Normal file
73
pkg/strategy/blocking_strategy_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockingStrategy_ServeHTTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
body string
|
||||
status int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "service keeps on starting",
|
||||
body: "starting",
|
||||
status: 200,
|
||||
expected: 503,
|
||||
},
|
||||
{
|
||||
desc: "service is started",
|
||||
body: "started",
|
||||
status: 200,
|
||||
expected: 200,
|
||||
},
|
||||
{
|
||||
desc: "ondemand service is in error",
|
||||
body: "error",
|
||||
status: 503,
|
||||
expected: 500,
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.status)
|
||||
fmt.Fprint(w, test.body)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
blockingStrategy := &BlockingStrategy{
|
||||
Name: "whoami",
|
||||
Request: 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, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
46
pkg/strategy/dynamic_strategy.go
Normal file
46
pkg/strategy/dynamic_strategy.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/acouvreur/traefik-ondemand-plugin/pkg/pages"
|
||||
)
|
||||
|
||||
type DynamicStrategy struct {
|
||||
Request string
|
||||
Name string
|
||||
Next http.Handler
|
||||
Timeout time.Duration
|
||||
LoadingPage string
|
||||
ErrorPage string
|
||||
}
|
||||
|
||||
// ServeHTTP retrieve the service status
|
||||
func (e *DynamicStrategy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
log.Printf("Sending request: %s", e.Request)
|
||||
status, err := getServiceStatus(e.Request)
|
||||
log.Printf("Status: %s", status)
|
||||
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, e.Name, err.Error())))
|
||||
}
|
||||
|
||||
if status == "started" {
|
||||
// Service started forward request
|
||||
e.Next.ServeHTTP(rw, req)
|
||||
|
||||
} else if status == "starting" {
|
||||
// Service starting, notify client
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
rw.Write([]byte(pages.GetLoadingPage(e.LoadingPage, e.Name, e.Timeout)))
|
||||
} else {
|
||||
// Error
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, e.Name, status)))
|
||||
}
|
||||
|
||||
}
|
||||
63
pkg/strategy/dynamic_strategy_test.go
Normal file
63
pkg/strategy/dynamic_strategy_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDynamicStrategy_ServeHTTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
status string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "service is starting",
|
||||
status: "starting",
|
||||
expected: 202,
|
||||
},
|
||||
{
|
||||
desc: "service is started",
|
||||
status: "started",
|
||||
expected: 200,
|
||||
},
|
||||
{
|
||||
desc: "ondemand service is in error",
|
||||
status: "error",
|
||||
expected: 500,
|
||||
},
|
||||
}
|
||||
|
||||
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) {})
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, test.status)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
dynamicStrategy := &DynamicStrategy{
|
||||
Name: "whoami",
|
||||
Request: 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, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
39
pkg/strategy/strategy.go
Normal file
39
pkg/strategy/strategy.go
Normal 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
|
||||
}
|
||||
11
release.config.js
Normal file
11
release.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
"branches": [
|
||||
{ "name": "main" },
|
||||
{ "name": "beta", "channel": "beta", "prerelease": "beta" },
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ api:
|
||||
|
||||
experimental:
|
||||
localPlugins:
|
||||
ondemand:
|
||||
traefik-ondemand-plugin:
|
||||
moduleName: github.com/acouvreur/traefik-ondemand-plugin
|
||||
|
||||
entryPoints:
|
||||
@@ -19,7 +19,4 @@ entryPoints:
|
||||
providers:
|
||||
docker:
|
||||
swarmMode: true
|
||||
exposedByDefault: false
|
||||
file:
|
||||
filename: "/etc/traefik/config.yml"
|
||||
watch: true
|
||||
exposedByDefault: false
|
||||
Reference in New Issue
Block a user