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:
Alexis Couvreur
2023-03-28 21:31:22 -04:00
committed by GitHub
parent f7b4c94c24
commit 795792058f
19 changed files with 403 additions and 83 deletions

View File

@@ -9,6 +9,7 @@ summary: "Start your containers on demand, shut them down automatically when the
testData:
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.
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
# You can only use one strategy at a time
# To do so, only declare `dynamic` or `blocking`

View File

@@ -21,6 +21,7 @@ Which allows you to start your containers on demand and shut them down automatic
- [Configuration File](#configuration-file)
- [Environment Variables](#environment-variables)
- [Arguments](#arguments)
- [](#)
- [Install Sablier on its own](#install-sablier-on-its-own)
- [Use the Docker image](#use-the-docker-image)
- [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)
- [Using the `/health` route](#using-the-health-route)
- [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)
- [GET `/api/strategies/dynamic`](#get-apistrategiesdynamic)
- [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) |
| Nginx | ✅ | ✅ | ❌ |
| Apache | *Coming soon*
| Apache | *Coming soon* |
| Caddy | [See #67](https://github.com/acouvreur/sablier/issues/67) |
### Traefik
@@ -213,6 +218,8 @@ Becomes
sablier start --strategy.dynamic.custom-themes-path /my/path
```
###
## Install Sablier on its own
You can install Sablier with the following flavors:
@@ -337,6 +344,68 @@ services:
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
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
| Parameter | Value | Description |
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `names` | array of string | The instances to be started |
| `session_duration` | duration [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) | The session duration for all services, which will reset at each subsequent calls |
| `show_details` *(optional)* | bool | The details about instances |
| `display_name` *(optional)* | string | The display name |
| `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 |
| Parameter | Value | Description |
| -------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `names` | array of string | The instances to be started (cannot be used with `group` parameter) |
| `group` | string | The instance group to be started (using `sablier.group=mygroup` labels) (cannot be used with `names` 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 |
| `show_details` *(optional)* | bool | The details about instances |
| `display_name` *(optional)* | string | The display name |
| `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
@@ -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
| Parameter | Value | Description |
| ---------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `names` | array of string | The instances to be started |
| `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 |
| Parameter | Value | Description |
| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `names` | array of string | The instances to be started (cannot be used with `group` parameter) |
| `group` | string | The instance group to be started (using `sablier.group=mygroup` labels) (cannot be used with `names` 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 |
| `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`.

View File

@@ -3,7 +3,8 @@ package models
import "time"
type BlockingRequest struct {
Names []string `form:"names" binding:"required"`
Names []string `form:"names"`
Group string `form:"group"`
SessionDuration time.Duration `form:"session_duration"`
Timeout time.Duration `form:"timeout"`
}

View File

@@ -5,7 +5,8 @@ import (
)
type DynamicRequest struct {
Names []string `form:"names" binding:"required"`
Group string `form:"group"`
Names []string `form:"names"`
ShowDetails bool `form:"show_details"`
DisplayName string `form:"display_name"`
Theme string `form:"theme"`

View File

@@ -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) {
}

View File

@@ -61,7 +61,17 @@ func (s *ServeStrategy) ServeDynamic(c *gin.Context) {
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() {
c.Header("X-Sablier-Session-Status", "ready")
@@ -119,7 +129,23 @@ func (s *ServeStrategy) ServeBlocking(c *gin.Context) {
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 {
c.Header("X-Sablier-Session-Status", "not-ready")

View File

@@ -25,6 +25,7 @@ import (
type SessionsManagerMock struct {
SessionState sessions.SessionState
sessions.Manager
}
func (s *SessionsManagerMock) RequestSession(names []string, duration time.Duration) *sessions.SessionState {

View File

@@ -5,9 +5,11 @@ import (
"errors"
"fmt"
"io"
"strings"
"github.com/acouvreur/sablier/app/instance"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
@@ -24,12 +26,44 @@ func NewDockerClassicProvider() (*DockerClassicProvider, error) {
log.Fatal(fmt.Errorf("%+v", "Could not connect to docker API"))
return nil, err
}
return &DockerClassicProvider{
Client: cli,
desiredReplicas: 1,
}, 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) {
ctx := context.Background()
@@ -111,25 +145,22 @@ func (provider *DockerClassicProvider) NotifyInstanceStopped(ctx context.Context
msgs, errs := provider.Client.Events(ctx, types.EventsOptions{
Filters: filters.NewArgs(
filters.Arg("scope", "local"),
filters.Arg("type", "container"),
filters.Arg("type", events.ContainerEventType),
filters.Arg("event", "die"),
),
})
go func() {
for {
select {
case msg := <-msgs:
// Send the container that has died to the channel
instance <- msg.Actor.Attributes["name"]
case err := <-errs:
if errors.Is(err, io.EOF) {
log.Debug("provider event stream closed")
return
}
case <-ctx.Done():
for {
select {
case msg := <-msgs:
// Send the container that has died to the channel
instance <- strings.TrimPrefix(msg.Actor.Attributes["name"], "/")
case err := <-errs:
if errors.Is(err, io.EOF) {
log.Debug("provider event stream closed")
return
}
case <-ctx.Done():
return
}
}()
}
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strings"
"sync"
"github.com/acouvreur/sablier/app/instance"
"github.com/docker/docker/api/types"
@@ -17,6 +18,8 @@ import (
type DockerSwarmProvider struct {
Client client.APIClient
updateGroups chan any
groups *sync.Map
desiredReplicas int
}
@@ -28,7 +31,10 @@ func NewDockerSwarmProvider() (*DockerSwarmProvider, error) {
return &DockerSwarmProvider{
Client: cli,
desiredReplicas: 1,
updateGroups: make(chan any, 1),
groups: &sync.Map{},
}, nil
}
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)
}
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) {
ctx := context.Background()

View File

@@ -69,9 +69,11 @@ func NewKubernetesProvider() (*KubernetesProvider, error) {
if err != nil {
return nil, err
}
return &KubernetesProvider{
Client: client,
}, nil
}
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) {
ctx := context.Background()
@@ -174,10 +202,10 @@ func (provider *KubernetesProvider) getStatefulsetState(config *Config) (instanc
func (provider *KubernetesProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) {
inforemer := provider.watchDeployents(instance)
go inforemer.Run(ctx.Done())
inforemer = provider.watchStatefulSets(instance)
go inforemer.Run(ctx.Done())
informer := provider.watchDeployents(instance)
go informer.Run(ctx.Done())
informer = provider.watchStatefulSets(instance)
go informer.Run(ctx.Done())
}
func (provider *KubernetesProvider) watchDeployents(instance chan<- string) cache.SharedIndexInformer {

View File

@@ -8,10 +8,15 @@ import (
"github.com/acouvreur/sablier/config"
)
const enableLabel = "sablier.enable"
const groupLabel = "sablier.group"
const defaultGroupValue = "default"
type Provider interface {
Start(name string) (instance.State, error)
Stop(name string) (instance.State, error)
GetState(name string) (instance.State, error)
GetGroups() (map[string][]string, error)
NotifyInstanceStopped(ctx context.Context, instance chan<- string)
}

View 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
}
}
}
}

View File

@@ -53,6 +53,10 @@ func (provider *ProviderMock) GetState(name string) (instance.State, error) {
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 {
wg sync.WaitGroup

View File

@@ -14,9 +14,13 @@ import (
log "github.com/sirupsen/logrus"
)
const defaultRefreshFrequency = 2 * time.Second
type Manager interface {
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)
RequestReadySessionGroup(ctx context.Context, group string, duration time.Duration, timeout time.Duration) (*SessionState, error)
LoadSessions(io.ReadCloser) error
SaveSessions(io.WriteCloser) error
@@ -25,36 +29,58 @@ type Manager interface {
}
type SessionsManager struct {
events context.Context
ctx context.Context
cancel context.CancelFunc
store tinykv.KV[instance.State]
provider providers.Provider
instanceStopped chan string
store tinykv.KV[instance.State]
provider providers.Provider
groups map[string][]string
}
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)
go sm.provider.NotifyInstanceStopped(sm.ctx, instanceStopped)
go sm.consumeInstanceStopped(instanceStopped)
}
go func() {
for instance := range instanceStopped {
// Will delete from the store containers that have been stop either by external sources
// 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)
}
}()
func (sm *SessionsManager) consumeGroups(receive chan map[string][]string) {
for groups := range receive {
sm.groups = groups
}
}
events, cancel := context.WithCancel(context.Background())
provider.NotifyInstanceStopped(events, instanceStopped)
return &SessionsManager{
events: events,
cancel: cancel,
store: store,
provider: provider,
instanceStopped: instanceStopped,
func (sm *SessionsManager) consumeInstanceStopped(instanceStopped chan string) {
for instance := range instanceStopped {
// Will delete from the store containers that have been stop either by external sources
// 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)
sm.store.Delete(instance)
}
}
@@ -136,6 +162,21 @@ func (s *SessionsManager) RequestSession(names []string, duration time.Duration)
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) {
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) {
s.store.Put(instance.Name, *instance, duration)
}
@@ -225,9 +281,6 @@ func (s *SessionsManager) Stop() {
// Stop event listeners
s.cancel()
// Stop receiving stopped instance
close(s.instanceStopped)
// Stop the store
s.store.Stop()
}

View File

@@ -31,6 +31,11 @@ services:
- 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
# 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:
image: containous/whoami:v1.5.0
@@ -40,3 +45,6 @@ services:
# - traefik.enable
# - traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)
# - traefik.http.routers.whoami.middlewares=dynamic@docker
labels:
- sablier.enable=true
- sablier.group=whoami

View File

@@ -69,6 +69,7 @@ You can configure the middleware behavior with the following variables:
- `set $sablierUrl` The internal routing to reach Sablier API
- `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 $sablierNginxInternalRedirect` The internal location for the service to redirect e.g. @nginx

View File

@@ -22,6 +22,7 @@ function call(r) {
* @typedef {Object} SablierConfig
* @property {string} sablierUrl
* @property {string} names
* @property {string} group
* @property {string} sessionDuration
* @property {string} internalRedirect
* @property {string} displayName
@@ -41,6 +42,7 @@ function createConfigurationFromVariables(r) {
return {
sablierUrl: r.variables.sablierUrl,
names: r.variables.sablierNames,
group: r.variables.sablierGroup,
sessionDuration: r.variables.sablierSessionDuration,
internalRedirect: r.variables.sablierNginxInternalRedirect,

View File

@@ -22,6 +22,7 @@ type BlockingConfiguration struct {
type Config struct {
SablierURL string `yaml:"sablierUrl"`
Names string `yaml:"names"`
Group string `yaml:"group"`
SessionDuration string `yaml:"sessionDuration"`
splittedNames []string
Dynamic *DynamicConfiguration `yaml:"dynamic"`
@@ -32,6 +33,7 @@ func CreateConfig() *Config {
return &Config{
SablierURL: "http://sablier:10000",
Names: "",
Group: "",
SessionDuration: "",
splittedNames: []string{},
Dynamic: nil,
@@ -50,10 +52,12 @@ func (c *Config) BuildRequest(middlewareName string) (*http.Request, error) {
names[i] = strings.TrimSpace(names[i])
}
c.splittedNames = names
if len(names) >= 1 && len(names[0]) > 0 {
c.splittedNames = names
}
if len(names) == 0 {
return nil, fmt.Errorf("you must specify at least one name")
if len(names) == 0 && len(c.Group) == 0 {
return nil, fmt.Errorf("you must specify at least one name or a group")
}
if c.Dynamic != nil && c.Blocking != nil {
@@ -94,6 +98,10 @@ func (c *Config) buildDynamicRequest(middlewareName string) (*http.Request, erro
q.Add("names", name)
}
if c.Group != "" {
q.Add("group", c.Group)
}
if c.Dynamic.DisplayName != "" {
q.Add("display_name", c.Dynamic.DisplayName)
} else {
@@ -150,6 +158,10 @@ func (c *Config) buildBlockingRequest() (*http.Request, error) {
q.Add("names", name)
}
if c.Group != "" {
q.Add("group", c.Group)
}
if c.Blocking.Timeout != "" {
_, err := time.ParseDuration(c.Blocking.Timeout)

View File

@@ -16,6 +16,7 @@ func TestConfig_BuildRequest(t *testing.T) {
type fields struct {
SablierURL string
Names string
Group string
SessionDuration string
Dynamic *traefik.DynamicConfiguration
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),
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",
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),
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",
fields: fields{
@@ -218,6 +241,7 @@ func TestConfig_BuildRequest(t *testing.T) {
c := &traefik.Config{
SablierURL: tt.fields.SablierURL,
Names: tt.fields.Names,
Group: tt.fields.Group,
SessionDuration: tt.fields.SessionDuration,
Dynamic: tt.fields.Dynamic,
Blocking: tt.fields.Blocking,