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:
|
||||
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`
|
||||
|
||||
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)
|
||||
- [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`.
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
type SessionsManagerMock struct {
|
||||
SessionState sessions.SessionState
|
||||
sessions.Manager
|
||||
}
|
||||
|
||||
func (s *SessionsManagerMock) RequestSession(names []string, duration time.Duration) *sessions.SessionState {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (provider *ProviderMock) GetGroups() (map[string][]string, error) {
|
||||
return make(map[string][]string), nil
|
||||
}
|
||||
|
||||
type KVMock[T any] struct {
|
||||
wg sync.WaitGroup
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user