mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-21 13:23:03 +01:00
feat: add filter by labels (#134)
You are now able to use labels on containers and services such as `--sablier.enable=true` and `--sablier.group=mygroup` to select groups.
This commit is contained in:
@@ -9,6 +9,7 @@ summary: "Start your containers on demand, shut them down automatically when the
|
|||||||
testData:
|
testData:
|
||||||
sablierUrl: http://sablier:10000 # The sablier URL service, must be reachable from the Traefik instance
|
sablierUrl: http://sablier:10000 # The sablier URL service, must be reachable from the Traefik instance
|
||||||
names: whoami,nginx # Comma separated names of containers/services/deployments etc.
|
names: whoami,nginx # Comma separated names of containers/services/deployments etc.
|
||||||
|
group: default # Group name to use to filter by label, ignored if names is set
|
||||||
sessionDuration: 1m # The session duration after which containers/services/deployments instances are shutdown
|
sessionDuration: 1m # The session duration after which containers/services/deployments instances are shutdown
|
||||||
# You can only use one strategy at a time
|
# You can only use one strategy at a time
|
||||||
# To do so, only declare `dynamic` or `blocking`
|
# To do so, only declare `dynamic` or `blocking`
|
||||||
|
|||||||
99
README.md
99
README.md
@@ -21,6 +21,7 @@ Which allows you to start your containers on demand and shut them down automatic
|
|||||||
- [Configuration File](#configuration-file)
|
- [Configuration File](#configuration-file)
|
||||||
- [Environment Variables](#environment-variables)
|
- [Environment Variables](#environment-variables)
|
||||||
- [Arguments](#arguments)
|
- [Arguments](#arguments)
|
||||||
|
- [](#)
|
||||||
- [Install Sablier on its own](#install-sablier-on-its-own)
|
- [Install Sablier on its own](#install-sablier-on-its-own)
|
||||||
- [Use the Docker image](#use-the-docker-image)
|
- [Use the Docker image](#use-the-docker-image)
|
||||||
- [Use the binary distribution](#use-the-binary-distribution)
|
- [Use the binary distribution](#use-the-binary-distribution)
|
||||||
@@ -33,6 +34,10 @@ Which allows you to start your containers on demand and shut them down automatic
|
|||||||
- [Sablier Healthcheck](#sablier-healthcheck)
|
- [Sablier Healthcheck](#sablier-healthcheck)
|
||||||
- [Using the `/health` route](#using-the-health-route)
|
- [Using the `/health` route](#using-the-health-route)
|
||||||
- [Using the `sablier health` command](#using-the-sablier-health-command)
|
- [Using the `sablier health` command](#using-the-sablier-health-command)
|
||||||
|
- [Autodiscovery using labels](#autodiscovery-using-labels)
|
||||||
|
- [Docker labels](#docker-labels)
|
||||||
|
- [Docker swarm service labels](#docker-swarm-service-labels)
|
||||||
|
- [Kubernetes deployments labels](#kubernetes-deployments-labels)
|
||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [GET `/api/strategies/dynamic`](#get-apistrategiesdynamic)
|
- [GET `/api/strategies/dynamic`](#get-apistrategiesdynamic)
|
||||||
- [GET `/api/strategies/blocking`](#get-apistrategiesblocking)
|
- [GET `/api/strategies/blocking`](#get-apistrategiesblocking)
|
||||||
@@ -85,7 +90,7 @@ It leverage the API calls to Sablier to your reverse proxy middleware to wake up
|
|||||||
| ------------- | :-------------------------------------------------------: | :---------------: | :-----------: | :-------------------------------------------------------: |
|
| ------------- | :-------------------------------------------------------: | :---------------: | :-----------: | :-------------------------------------------------------: |
|
||||||
| Traefik | ✅ | ✅ | ✅ *(partial)* | [See #70](https://github.com/acouvreur/sablier/issues/70) |
|
| Traefik | ✅ | ✅ | ✅ *(partial)* | [See #70](https://github.com/acouvreur/sablier/issues/70) |
|
||||||
| Nginx | ✅ | ✅ | ❌ |
|
| Nginx | ✅ | ✅ | ❌ |
|
||||||
| Apache | *Coming soon*
|
| Apache | *Coming soon* |
|
||||||
| Caddy | [See #67](https://github.com/acouvreur/sablier/issues/67) |
|
| Caddy | [See #67](https://github.com/acouvreur/sablier/issues/67) |
|
||||||
|
|
||||||
### Traefik
|
### Traefik
|
||||||
@@ -213,6 +218,8 @@ Becomes
|
|||||||
sablier start --strategy.dynamic.custom-themes-path /my/path
|
sablier start --strategy.dynamic.custom-themes-path /my/path
|
||||||
```
|
```
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
## Install Sablier on its own
|
## Install Sablier on its own
|
||||||
|
|
||||||
You can install Sablier with the following flavors:
|
You can install Sablier with the following flavors:
|
||||||
@@ -337,6 +344,68 @@ services:
|
|||||||
interval: 1m30s
|
interval: 1m30s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Autodiscovery using labels
|
||||||
|
|
||||||
|
Instead of specifying the names of the instances you want to use, you can take advantage of the labels to specify groups of containers.
|
||||||
|
|
||||||
|
- `sablier.enable=true`
|
||||||
|
- `sablier.group=mygroup` (*optional*) defaults to "default"
|
||||||
|
|
||||||
|
You can then use the API by specifying the group instead of the container names.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X GET -v "http://localhost:10000/api/strategies/blocking?group=mygroup&session_duration=5m&timeout=5s"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker labels
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami:v1.5.0
|
||||||
|
labels:
|
||||||
|
- sablier.enable=true
|
||||||
|
- sablier.group=mygroup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker swarm service labels
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami:v1.5.0
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- sablier.enable=true
|
||||||
|
- sablier.group=mygroup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes deployments labels
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami-deployment
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
sablier.enable: true
|
||||||
|
sablier.group: mygroup
|
||||||
|
spec:
|
||||||
|
replicas: 0
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whoami
|
||||||
|
image: containous/whoami:v1.5.0
|
||||||
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
To run the following examples you can create two containers:
|
To run the following examples you can create two containers:
|
||||||
@@ -348,14 +417,15 @@ To run the following examples you can create two containers:
|
|||||||
|
|
||||||
**Description**: The `/api/strategies/dynamic` endpoint allows you to request a waiting page for multiple instances
|
**Description**: The `/api/strategies/dynamic` endpoint allows you to request a waiting page for multiple instances
|
||||||
|
|
||||||
| Parameter | Value | Description |
|
| Parameter | Value | Description |
|
||||||
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
| -------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| `names` | array of string | The instances to be started |
|
| `names` | array of string | The instances to be started (cannot be used with `group` parameter) |
|
||||||
| `session_duration` | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The session duration for all services, which will reset at each subsequent calls |
|
| `group` | string | The instance group to be started (using `sablier.group=mygroup` labels) (cannot be used with `names` parameter) |
|
||||||
| `show_details` *(optional)* | bool | The details about instances |
|
| `session_duration` | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The session duration for all services, which will reset at each subsequent calls |
|
||||||
| `display_name` *(optional)* | string | The display name |
|
| `show_details` *(optional)* | bool | The details about instances |
|
||||||
| `theme` *(optional)* | string | The theme to use |
|
| `display_name` *(optional)* | string | The display name |
|
||||||
| `refresh_frequency` *(optional)* | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The refresh frequency for the loading page |
|
| `theme` *(optional)* | string | The theme to use |
|
||||||
|
| `refresh_frequency` *(optional)* | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The refresh frequency for the loading page |
|
||||||
|
|
||||||
Go to http://localhost:10000/api/strategies/dynamic?names=nginx&names=apache&session_duration=5m&show_details=true&display_name=example&theme=hacker-terminal&refresh_frequency=10s and you should see
|
Go to http://localhost:10000/api/strategies/dynamic?names=nginx&names=apache&session_duration=5m&show_details=true&display_name=example&theme=hacker-terminal&refresh_frequency=10s and you should see
|
||||||
|
|
||||||
@@ -367,11 +437,12 @@ A special header `X-Sablier-Session-Status` is returned and will have the value
|
|||||||
|
|
||||||
**Description**: The `/api/strategies/blocking` endpoint allows you to wait until the instances are ready
|
**Description**: The `/api/strategies/blocking` endpoint allows you to wait until the instances are ready
|
||||||
|
|
||||||
| Parameter | Value | Description |
|
| Parameter | Value | Description |
|
||||||
| ---------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| `names` | array of string | The instances to be started |
|
| `names` | array of string | The instances to be started (cannot be used with `group` parameter) |
|
||||||
| `session_duration` | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The session duration for all services, which will reset at each subsequent calls |
|
| `group` | string | The instance group to be started (using `sablier.group=mygroup` labels) (cannot be used with `names` parameter) |
|
||||||
| `timeout` *(optional)* | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The maximum time to wait for instances to be ready |
|
| `session_duration` | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The session duration for all services, which will reset at each subsequent calls |
|
||||||
|
| `timeout` *(optional)* | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The maximum time to wait for instances to be ready |
|
||||||
|
|
||||||
A special header `X-Sablier-Session-Status` is returned and will have the value `ready` if all instances are ready. Or else `not-ready`.
|
A special header `X-Sablier-Session-Status` is returned and will have the value `ready` if all instances are ready. Or else `not-ready`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package models
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type BlockingRequest struct {
|
type BlockingRequest struct {
|
||||||
Names []string `form:"names" binding:"required"`
|
Names []string `form:"names"`
|
||||||
|
Group string `form:"group"`
|
||||||
SessionDuration time.Duration `form:"session_duration"`
|
SessionDuration time.Duration `form:"session_duration"`
|
||||||
Timeout time.Duration `form:"timeout"`
|
Timeout time.Duration `form:"timeout"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DynamicRequest struct {
|
type DynamicRequest struct {
|
||||||
Names []string `form:"names" binding:"required"`
|
Group string `form:"group"`
|
||||||
|
Names []string `form:"names"`
|
||||||
ShowDetails bool `form:"show_details"`
|
ShowDetails bool `form:"show_details"`
|
||||||
DisplayName string `form:"display_name"`
|
DisplayName string `form:"display_name"`
|
||||||
Theme string `form:"theme"`
|
Theme string `form:"theme"`
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
func GetSessions(c *gin.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSession(c *gin.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutSession(c *gin.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteSession(c *gin.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -61,7 +61,17 @@ func (s *ServeStrategy) ServeDynamic(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionState := s.SessionsManager.RequestSession(request.Names, request.SessionDuration)
|
var sessionState *sessions.SessionState
|
||||||
|
if len(request.Names) > 0 {
|
||||||
|
sessionState = s.SessionsManager.RequestSession(request.Names, request.SessionDuration)
|
||||||
|
} else {
|
||||||
|
sessionState = s.SessionsManager.RequestSessionGroup(request.Group, request.SessionDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionState == nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if sessionState.IsReady() {
|
if sessionState.IsReady() {
|
||||||
c.Header("X-Sablier-Session-Status", "ready")
|
c.Header("X-Sablier-Session-Status", "ready")
|
||||||
@@ -119,7 +129,23 @@ func (s *ServeStrategy) ServeBlocking(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionState, err := s.SessionsManager.RequestReadySession(c.Request.Context(), request.Names, request.SessionDuration, request.Timeout)
|
var sessionState *sessions.SessionState
|
||||||
|
var err error
|
||||||
|
if len(request.Names) > 0 {
|
||||||
|
sessionState, err = s.SessionsManager.RequestReadySession(c.Request.Context(), request.Names, request.SessionDuration, request.Timeout)
|
||||||
|
} else {
|
||||||
|
sessionState, err = s.SessionsManager.RequestReadySessionGroup(c.Request.Context(), request.Group, request.SessionDuration, request.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionState == nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Header("X-Sablier-Session-Status", "not-ready")
|
c.Header("X-Sablier-Session-Status", "not-ready")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
type SessionsManagerMock struct {
|
type SessionsManagerMock struct {
|
||||||
SessionState sessions.SessionState
|
SessionState sessions.SessionState
|
||||||
|
sessions.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionsManagerMock) RequestSession(names []string, duration time.Duration) *sessions.SessionState {
|
func (s *SessionsManagerMock) RequestSession(names []string, duration time.Duration) *sessions.SessionState {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/acouvreur/sablier/app/instance"
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -24,12 +26,44 @@ func NewDockerClassicProvider() (*DockerClassicProvider, error) {
|
|||||||
log.Fatal(fmt.Errorf("%+v", "Could not connect to docker API"))
|
log.Fatal(fmt.Errorf("%+v", "Could not connect to docker API"))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DockerClassicProvider{
|
return &DockerClassicProvider{
|
||||||
Client: cli,
|
Client: cli,
|
||||||
desiredReplicas: 1,
|
desiredReplicas: 1,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *DockerClassicProvider) GetGroups() (map[string][]string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
filters := filters.NewArgs()
|
||||||
|
filters.Add("label", fmt.Sprintf("%s=true", enableLabel))
|
||||||
|
|
||||||
|
containers, err := provider.Client.ContainerList(ctx, types.ContainerListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string][]string)
|
||||||
|
for _, container := range containers {
|
||||||
|
groupName := container.Labels[groupLabel]
|
||||||
|
if len(groupName) == 0 {
|
||||||
|
groupName = defaultGroupValue
|
||||||
|
}
|
||||||
|
group := groups[groupName]
|
||||||
|
group = append(group, strings.TrimPrefix(container.Names[0], "/"))
|
||||||
|
groups[groupName] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(fmt.Sprintf("%v", groups))
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *DockerClassicProvider) Start(name string) (instance.State, error) {
|
func (provider *DockerClassicProvider) Start(name string) (instance.State, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -111,25 +145,22 @@ func (provider *DockerClassicProvider) NotifyInstanceStopped(ctx context.Context
|
|||||||
msgs, errs := provider.Client.Events(ctx, types.EventsOptions{
|
msgs, errs := provider.Client.Events(ctx, types.EventsOptions{
|
||||||
Filters: filters.NewArgs(
|
Filters: filters.NewArgs(
|
||||||
filters.Arg("scope", "local"),
|
filters.Arg("scope", "local"),
|
||||||
filters.Arg("type", "container"),
|
filters.Arg("type", events.ContainerEventType),
|
||||||
filters.Arg("event", "die"),
|
filters.Arg("event", "die"),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
for {
|
||||||
go func() {
|
select {
|
||||||
for {
|
case msg := <-msgs:
|
||||||
select {
|
// Send the container that has died to the channel
|
||||||
case msg := <-msgs:
|
instance <- strings.TrimPrefix(msg.Actor.Attributes["name"], "/")
|
||||||
// Send the container that has died to the channel
|
case err := <-errs:
|
||||||
instance <- msg.Actor.Attributes["name"]
|
if errors.Is(err, io.EOF) {
|
||||||
case err := <-errs:
|
log.Debug("provider event stream closed")
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
log.Debug("provider event stream closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/acouvreur/sablier/app/instance"
|
"github.com/acouvreur/sablier/app/instance"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@@ -17,6 +18,8 @@ import (
|
|||||||
|
|
||||||
type DockerSwarmProvider struct {
|
type DockerSwarmProvider struct {
|
||||||
Client client.APIClient
|
Client client.APIClient
|
||||||
|
updateGroups chan any
|
||||||
|
groups *sync.Map
|
||||||
desiredReplicas int
|
desiredReplicas int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,10 @@ func NewDockerSwarmProvider() (*DockerSwarmProvider, error) {
|
|||||||
return &DockerSwarmProvider{
|
return &DockerSwarmProvider{
|
||||||
Client: cli,
|
Client: cli,
|
||||||
desiredReplicas: 1,
|
desiredReplicas: 1,
|
||||||
|
updateGroups: make(chan any, 1),
|
||||||
|
groups: &sync.Map{},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *DockerSwarmProvider) Start(name string) (instance.State, error) {
|
func (provider *DockerSwarmProvider) Start(name string) (instance.State, error) {
|
||||||
@@ -68,6 +74,43 @@ func (provider *DockerSwarmProvider) scale(name string, replicas uint64) (instan
|
|||||||
return instance.NotReadyInstanceState(foundName, 0, provider.desiredReplicas)
|
return instance.NotReadyInstanceState(foundName, 0, provider.desiredReplicas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *DockerSwarmProvider) GetGroups() (map[string][]string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
filters := filters.NewArgs()
|
||||||
|
filters.Add("label", fmt.Sprintf("%s=true", enableLabel))
|
||||||
|
|
||||||
|
services, err := provider.Client.ServiceList(ctx, types.ServiceListOptions{
|
||||||
|
Filters: filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string][]string)
|
||||||
|
for _, service := range services {
|
||||||
|
groupName := service.Spec.Labels[groupLabel]
|
||||||
|
if len(groupName) == 0 {
|
||||||
|
groupName = defaultGroupValue
|
||||||
|
}
|
||||||
|
|
||||||
|
group := groups[groupName]
|
||||||
|
group = append(group, service.Spec.Name)
|
||||||
|
groups[groupName] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *DockerSwarmProvider) GetGroup(group string) []string {
|
||||||
|
containers, ok := provider.groups.Load(group)
|
||||||
|
if !ok {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return containers.([]string)
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *DockerSwarmProvider) GetState(name string) (instance.State, error) {
|
func (provider *DockerSwarmProvider) GetState(name string) (instance.State, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ func NewKubernetesProvider() (*KubernetesProvider, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &KubernetesProvider{
|
return &KubernetesProvider{
|
||||||
Client: client,
|
Client: client,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *KubernetesProvider) Start(name string) (instance.State, error) {
|
func (provider *KubernetesProvider) Start(name string) (instance.State, error) {
|
||||||
@@ -93,6 +95,32 @@ func (provider *KubernetesProvider) Stop(name string) (instance.State, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *KubernetesProvider) GetGroups() (map[string][]string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
deployments, err := provider.Client.AppsV1().Deployments(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{
|
||||||
|
LabelSelector: enableLabel,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string][]string)
|
||||||
|
for _, deployment := range deployments.Items {
|
||||||
|
groupName := deployment.Labels[groupLabel]
|
||||||
|
if len(groupName) == 0 {
|
||||||
|
groupName = defaultGroupValue
|
||||||
|
}
|
||||||
|
|
||||||
|
group := groups[groupName]
|
||||||
|
group = append(group, deployment.Name)
|
||||||
|
groups[groupName] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *KubernetesProvider) scale(config *Config, replicas int32) (instance.State, error) {
|
func (provider *KubernetesProvider) scale(config *Config, replicas int32) (instance.State, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -174,10 +202,10 @@ func (provider *KubernetesProvider) getStatefulsetState(config *Config) (instanc
|
|||||||
|
|
||||||
func (provider *KubernetesProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) {
|
func (provider *KubernetesProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) {
|
||||||
|
|
||||||
inforemer := provider.watchDeployents(instance)
|
informer := provider.watchDeployents(instance)
|
||||||
go inforemer.Run(ctx.Done())
|
go informer.Run(ctx.Done())
|
||||||
inforemer = provider.watchStatefulSets(instance)
|
informer = provider.watchStatefulSets(instance)
|
||||||
go inforemer.Run(ctx.Done())
|
go informer.Run(ctx.Done())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *KubernetesProvider) watchDeployents(instance chan<- string) cache.SharedIndexInformer {
|
func (provider *KubernetesProvider) watchDeployents(instance chan<- string) cache.SharedIndexInformer {
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import (
|
|||||||
"github.com/acouvreur/sablier/config"
|
"github.com/acouvreur/sablier/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const enableLabel = "sablier.enable"
|
||||||
|
const groupLabel = "sablier.group"
|
||||||
|
const defaultGroupValue = "default"
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Start(name string) (instance.State, error)
|
Start(name string) (instance.State, error)
|
||||||
Stop(name string) (instance.State, error)
|
Stop(name string) (instance.State, error)
|
||||||
GetState(name string) (instance.State, error)
|
GetState(name string) (instance.State, error)
|
||||||
|
GetGroups() (map[string][]string, error)
|
||||||
|
|
||||||
NotifyInstanceStopped(ctx context.Context, instance chan<- string)
|
NotifyInstanceStopped(ctx context.Context, instance chan<- string)
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/sessions/groups_watcher.go
Normal file
27
app/sessions/groups_watcher.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/app/providers"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// watchGroups watches indefinitely for new groups
|
||||||
|
func watchGroups(ctx context.Context, provider providers.Provider, frequency time.Duration, send chan<- map[string][]string) {
|
||||||
|
ticker := time.NewTicker(frequency)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
groups, err := provider.GetGroups()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("could not get groups", err)
|
||||||
|
} else {
|
||||||
|
send <- groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,10 @@ func (provider *ProviderMock) GetState(name string) (instance.State, error) {
|
|||||||
return args.Get(0).(instance.State), args.Error(1)
|
return args.Get(0).(instance.State), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *ProviderMock) GetGroups() (map[string][]string, error) {
|
||||||
|
return make(map[string][]string), nil
|
||||||
|
}
|
||||||
|
|
||||||
type KVMock[T any] struct {
|
type KVMock[T any] struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultRefreshFrequency = 2 * time.Second
|
||||||
|
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
RequestSession(names []string, duration time.Duration) *SessionState
|
RequestSession(names []string, duration time.Duration) *SessionState
|
||||||
|
RequestSessionGroup(group string, duration time.Duration) *SessionState
|
||||||
RequestReadySession(ctx context.Context, names []string, duration time.Duration, timeout time.Duration) (*SessionState, error)
|
RequestReadySession(ctx context.Context, names []string, duration time.Duration, timeout time.Duration) (*SessionState, error)
|
||||||
|
RequestReadySessionGroup(ctx context.Context, group string, duration time.Duration, timeout time.Duration) (*SessionState, error)
|
||||||
|
|
||||||
LoadSessions(io.ReadCloser) error
|
LoadSessions(io.ReadCloser) error
|
||||||
SaveSessions(io.WriteCloser) error
|
SaveSessions(io.WriteCloser) error
|
||||||
@@ -25,36 +29,58 @@ type Manager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SessionsManager struct {
|
type SessionsManager struct {
|
||||||
events context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
store tinykv.KV[instance.State]
|
store tinykv.KV[instance.State]
|
||||||
provider providers.Provider
|
provider providers.Provider
|
||||||
instanceStopped chan string
|
groups map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSessionsManager(store tinykv.KV[instance.State], provider providers.Provider) Manager {
|
func NewSessionsManager(store tinykv.KV[instance.State], provider providers.Provider) Manager {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
groups, err := provider.GetGroups()
|
||||||
|
if err != nil {
|
||||||
|
groups = make(map[string][]string)
|
||||||
|
log.Warn("could not get groups", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm := &SessionsManager{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
store: store,
|
||||||
|
provider: provider,
|
||||||
|
groups: groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.initWatchers()
|
||||||
|
|
||||||
|
return sm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SessionsManager) initWatchers() {
|
||||||
|
updateGroups := make(chan map[string][]string)
|
||||||
|
go watchGroups(sm.ctx, sm.provider, defaultRefreshFrequency, updateGroups)
|
||||||
|
go sm.consumeGroups(updateGroups)
|
||||||
|
|
||||||
instanceStopped := make(chan string)
|
instanceStopped := make(chan string)
|
||||||
|
go sm.provider.NotifyInstanceStopped(sm.ctx, instanceStopped)
|
||||||
|
go sm.consumeInstanceStopped(instanceStopped)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
func (sm *SessionsManager) consumeGroups(receive chan map[string][]string) {
|
||||||
for instance := range instanceStopped {
|
for groups := range receive {
|
||||||
// Will delete from the store containers that have been stop either by external sources
|
sm.groups = groups
|
||||||
// or by the internal expiration loop, if the deleted entry does not exist, it doesn't matter
|
}
|
||||||
log.Debugf("received event instance %s is stopped, removing from store", instance)
|
}
|
||||||
store.Delete(instance)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
events, cancel := context.WithCancel(context.Background())
|
func (sm *SessionsManager) consumeInstanceStopped(instanceStopped chan string) {
|
||||||
provider.NotifyInstanceStopped(events, instanceStopped)
|
for instance := range instanceStopped {
|
||||||
|
// Will delete from the store containers that have been stop either by external sources
|
||||||
return &SessionsManager{
|
// or by the internal expiration loop, if the deleted entry does not exist, it doesn't matter
|
||||||
events: events,
|
log.Debugf("received event instance %s is stopped, removing from store", instance)
|
||||||
cancel: cancel,
|
sm.store.Delete(instance)
|
||||||
store: store,
|
|
||||||
provider: provider,
|
|
||||||
instanceStopped: instanceStopped,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +162,21 @@ func (s *SessionsManager) RequestSession(names []string, duration time.Duration)
|
|||||||
return sessionState
|
return sessionState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) RequestSessionGroup(group string, duration time.Duration) (sessionState *SessionState) {
|
||||||
|
|
||||||
|
if len(group) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names := s.groups[group]
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.RequestSession(names, duration)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionsManager) requestSessionInstance(name string, duration time.Duration) (*instance.State, error) {
|
func (s *SessionsManager) requestSessionInstance(name string, duration time.Duration) (*instance.State, error) {
|
||||||
|
|
||||||
requestState, exists := s.store.Get(name)
|
requestState, exists := s.store.Get(name)
|
||||||
@@ -217,6 +258,21 @@ func (s *SessionsManager) RequestReadySession(ctx context.Context, names []strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SessionsManager) RequestReadySessionGroup(ctx context.Context, group string, duration time.Duration, timeout time.Duration) (sessionState *SessionState, err error) {
|
||||||
|
|
||||||
|
if len(group) == 0 {
|
||||||
|
return nil, fmt.Errorf("group is mandatory")
|
||||||
|
}
|
||||||
|
|
||||||
|
names := s.groups[group]
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("group has no member")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.RequestReadySession(ctx, names, duration, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionsManager) ExpiresAfter(instance *instance.State, duration time.Duration) {
|
func (s *SessionsManager) ExpiresAfter(instance *instance.State, duration time.Duration) {
|
||||||
s.store.Put(instance.Name, *instance, duration)
|
s.store.Put(instance.Name, *instance, duration)
|
||||||
}
|
}
|
||||||
@@ -225,9 +281,6 @@ func (s *SessionsManager) Stop() {
|
|||||||
// Stop event listeners
|
// Stop event listeners
|
||||||
s.cancel()
|
s.cancel()
|
||||||
|
|
||||||
// Stop receiving stopped instance
|
|
||||||
close(s.instanceStopped)
|
|
||||||
|
|
||||||
// Stop the store
|
// Stop the store
|
||||||
s.store.Stop()
|
s.store.Stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ services:
|
|||||||
- traefik.http.middlewares.blocking.plugin.sablier.sablierUrl=http://sablier:10000
|
- traefik.http.middlewares.blocking.plugin.sablier.sablierUrl=http://sablier:10000
|
||||||
- traefik.http.middlewares.blocking.plugin.sablier.sessionDuration=1m
|
- traefik.http.middlewares.blocking.plugin.sablier.sessionDuration=1m
|
||||||
- traefik.http.middlewares.blocking.plugin.sablier.blocking.timeout=30s
|
- traefik.http.middlewares.blocking.plugin.sablier.blocking.timeout=30s
|
||||||
|
# Blocking Middleware
|
||||||
|
- traefik.http.middlewares.blocking.plugin.sablier.group=sablier
|
||||||
|
- traefik.http.middlewares.blocking.plugin.sablier.sablierUrl=http://sablier:10000
|
||||||
|
- traefik.http.middlewares.blocking.plugin.sablier.sessionDuration=1m
|
||||||
|
- traefik.http.middlewares.blocking.plugin.sablier.blocking.timeout=30s
|
||||||
|
|
||||||
whoami:
|
whoami:
|
||||||
image: containous/whoami:v1.5.0
|
image: containous/whoami:v1.5.0
|
||||||
@@ -40,3 +45,6 @@ services:
|
|||||||
# - traefik.enable
|
# - traefik.enable
|
||||||
# - traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)
|
# - traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)
|
||||||
# - traefik.http.routers.whoami.middlewares=dynamic@docker
|
# - traefik.http.routers.whoami.middlewares=dynamic@docker
|
||||||
|
labels:
|
||||||
|
- sablier.enable=true
|
||||||
|
- sablier.group=whoami
|
||||||
@@ -69,6 +69,7 @@ You can configure the middleware behavior with the following variables:
|
|||||||
|
|
||||||
- `set $sablierUrl` The internal routing to reach Sablier API
|
- `set $sablierUrl` The internal routing to reach Sablier API
|
||||||
- `set $sablierNames` Comma separated names of containers/services/deployments etc.
|
- `set $sablierNames` Comma separated names of containers/services/deployments etc.
|
||||||
|
- `set $sablierGroup` Group name to use to filter by label, ignored if sablierNames is set
|
||||||
- `set $sablierSessionDuration` The session duration after which containers/services/deployments instances are shutdown
|
- `set $sablierSessionDuration` The session duration after which containers/services/deployments instances are shutdown
|
||||||
- `set $sablierNginxInternalRedirect` The internal location for the service to redirect e.g. @nginx
|
- `set $sablierNginxInternalRedirect` The internal location for the service to redirect e.g. @nginx
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function call(r) {
|
|||||||
* @typedef {Object} SablierConfig
|
* @typedef {Object} SablierConfig
|
||||||
* @property {string} sablierUrl
|
* @property {string} sablierUrl
|
||||||
* @property {string} names
|
* @property {string} names
|
||||||
|
* @property {string} group
|
||||||
* @property {string} sessionDuration
|
* @property {string} sessionDuration
|
||||||
* @property {string} internalRedirect
|
* @property {string} internalRedirect
|
||||||
* @property {string} displayName
|
* @property {string} displayName
|
||||||
@@ -41,6 +42,7 @@ function createConfigurationFromVariables(r) {
|
|||||||
return {
|
return {
|
||||||
sablierUrl: r.variables.sablierUrl,
|
sablierUrl: r.variables.sablierUrl,
|
||||||
names: r.variables.sablierNames,
|
names: r.variables.sablierNames,
|
||||||
|
group: r.variables.sablierGroup,
|
||||||
sessionDuration: r.variables.sablierSessionDuration,
|
sessionDuration: r.variables.sablierSessionDuration,
|
||||||
internalRedirect: r.variables.sablierNginxInternalRedirect,
|
internalRedirect: r.variables.sablierNginxInternalRedirect,
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type BlockingConfiguration struct {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
SablierURL string `yaml:"sablierUrl"`
|
SablierURL string `yaml:"sablierUrl"`
|
||||||
Names string `yaml:"names"`
|
Names string `yaml:"names"`
|
||||||
|
Group string `yaml:"group"`
|
||||||
SessionDuration string `yaml:"sessionDuration"`
|
SessionDuration string `yaml:"sessionDuration"`
|
||||||
splittedNames []string
|
splittedNames []string
|
||||||
Dynamic *DynamicConfiguration `yaml:"dynamic"`
|
Dynamic *DynamicConfiguration `yaml:"dynamic"`
|
||||||
@@ -32,6 +33,7 @@ func CreateConfig() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
SablierURL: "http://sablier:10000",
|
SablierURL: "http://sablier:10000",
|
||||||
Names: "",
|
Names: "",
|
||||||
|
Group: "",
|
||||||
SessionDuration: "",
|
SessionDuration: "",
|
||||||
splittedNames: []string{},
|
splittedNames: []string{},
|
||||||
Dynamic: nil,
|
Dynamic: nil,
|
||||||
@@ -50,10 +52,12 @@ func (c *Config) BuildRequest(middlewareName string) (*http.Request, error) {
|
|||||||
names[i] = strings.TrimSpace(names[i])
|
names[i] = strings.TrimSpace(names[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
c.splittedNames = names
|
if len(names) >= 1 && len(names[0]) > 0 {
|
||||||
|
c.splittedNames = names
|
||||||
|
}
|
||||||
|
|
||||||
if len(names) == 0 {
|
if len(names) == 0 && len(c.Group) == 0 {
|
||||||
return nil, fmt.Errorf("you must specify at least one name")
|
return nil, fmt.Errorf("you must specify at least one name or a group")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Dynamic != nil && c.Blocking != nil {
|
if c.Dynamic != nil && c.Blocking != nil {
|
||||||
@@ -94,6 +98,10 @@ func (c *Config) buildDynamicRequest(middlewareName string) (*http.Request, erro
|
|||||||
q.Add("names", name)
|
q.Add("names", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Group != "" {
|
||||||
|
q.Add("group", c.Group)
|
||||||
|
}
|
||||||
|
|
||||||
if c.Dynamic.DisplayName != "" {
|
if c.Dynamic.DisplayName != "" {
|
||||||
q.Add("display_name", c.Dynamic.DisplayName)
|
q.Add("display_name", c.Dynamic.DisplayName)
|
||||||
} else {
|
} else {
|
||||||
@@ -150,6 +158,10 @@ func (c *Config) buildBlockingRequest() (*http.Request, error) {
|
|||||||
q.Add("names", name)
|
q.Add("names", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Group != "" {
|
||||||
|
q.Add("group", c.Group)
|
||||||
|
}
|
||||||
|
|
||||||
if c.Blocking.Timeout != "" {
|
if c.Blocking.Timeout != "" {
|
||||||
_, err := time.ParseDuration(c.Blocking.Timeout)
|
_, err := time.ParseDuration(c.Blocking.Timeout)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func TestConfig_BuildRequest(t *testing.T) {
|
|||||||
type fields struct {
|
type fields struct {
|
||||||
SablierURL string
|
SablierURL string
|
||||||
Names string
|
Names string
|
||||||
|
Group string
|
||||||
SessionDuration string
|
SessionDuration string
|
||||||
Dynamic *traefik.DynamicConfiguration
|
Dynamic *traefik.DynamicConfiguration
|
||||||
Blocking *traefik.BlockingConfiguration
|
Blocking *traefik.BlockingConfiguration
|
||||||
@@ -47,6 +48,17 @@ func TestConfig_BuildRequest(t *testing.T) {
|
|||||||
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?display_name=sablier-middleware&names=nginx&names=apache&session_duration=1m", nil),
|
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?display_name=sablier-middleware&names=nginx&names=apache&session_duration=1m", nil),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic session with group",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Group: "default",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Dynamic: &traefik.DynamicConfiguration{},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/dynamic?display_name=sablier-middleware&group=default&session_duration=1m", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "dynamic session with theme values",
|
name: "dynamic session with theme values",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
@@ -174,6 +186,17 @@ func TestConfig_BuildRequest(t *testing.T) {
|
|||||||
want: createRequest("GET", "http://sablier:10000/api/strategies/blocking?names=nginx&names=apache&session_duration=1m", nil),
|
want: createRequest("GET", "http://sablier:10000/api/strategies/blocking?names=nginx&names=apache&session_duration=1m", nil),
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "blocking session with group",
|
||||||
|
fields: fields{
|
||||||
|
SablierURL: "http://sablier:10000",
|
||||||
|
Group: "default",
|
||||||
|
SessionDuration: "1m",
|
||||||
|
Blocking: &traefik.BlockingConfiguration{},
|
||||||
|
},
|
||||||
|
want: createRequest("GET", "http://sablier:10000/api/strategies/blocking?group=default&session_duration=1m", nil),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "blocking session with timeout value",
|
name: "blocking session with timeout value",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
@@ -218,6 +241,7 @@ func TestConfig_BuildRequest(t *testing.T) {
|
|||||||
c := &traefik.Config{
|
c := &traefik.Config{
|
||||||
SablierURL: tt.fields.SablierURL,
|
SablierURL: tt.fields.SablierURL,
|
||||||
Names: tt.fields.Names,
|
Names: tt.fields.Names,
|
||||||
|
Group: tt.fields.Group,
|
||||||
SessionDuration: tt.fields.SessionDuration,
|
SessionDuration: tt.fields.SessionDuration,
|
||||||
Dynamic: tt.fields.Dynamic,
|
Dynamic: tt.fields.Dynamic,
|
||||||
Blocking: tt.fields.Blocking,
|
Blocking: tt.fields.Blocking,
|
||||||
|
|||||||
Reference in New Issue
Block a user