Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ http:
| `dynamic.theme` | string | No | Server default | Theme for the waiting page (e.g., `hacker-terminal`, `ghost`, `matrix`) |
| `dynamic.refreshFrequency` | duration | No | Server default | How often the waiting page checks if instances are ready |
| `blocking.timeout` | duration | No | - | Maximum time to wait for instances to become ready |
| `poke` | object | No | - | Poke strategy: start instances and immediately forward the request without waiting for readiness. |
| `ignoreUserAgent` | string or string\[\] | No | - | List of [Go regexp](https://pkg.go.dev/regexp/syntax) patterns. Requests whose `User-Agent` matches any pattern receive an immediate `HTTP 200` without waking the container. Can be specified as a YAML list (preferred) or as a single string (backward-compatible). Patterns are case-sensitive unless the `(?i)` flag is used. |

## Usage
Expand Down Expand Up @@ -126,7 +127,7 @@ http:
keepAliveInterval: 30s # (Optional) Re-ping Sablier every 30s to keep long-lived connections alive
failOpen: false # (Optional) Pass through when Sablier is unreachable instead of returning HTTP 500
# Only one strategy can be used at a time
# Declare either `dynamic` or `blocking`, not both
# Declare either `dynamic`, `blocking`, or `poke`
ignoreUserAgent: # (Optional) List of regexp patterns; matching UAs are silently ignored
- curl
- "(?i)uptimerobot"
Expand All @@ -142,6 +143,9 @@ http:
# Blocking strategy: waits for services to start (up to the timeout limit)
# blocking:
# timeout: 1m

# Poke strategy: starts services and immediately forwards the request
# poke: {}
```

<!-- TODO: Add usage example in a route -->
Expand Down
62 changes: 57 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type BlockingConfiguration struct {
Timeout string `yaml:"timeout"`
}

type PokeConfiguration struct {
}

type Config struct {
SablierURL string `yaml:"sablierUrl"`
// Deprecated: use Group instead
Expand All @@ -61,6 +64,7 @@ type Config struct {
splittedNames []string
Dynamic *DynamicConfiguration `yaml:"dynamic"`
Blocking *BlockingConfiguration `yaml:"blocking"`
Poke *PokeConfiguration `yaml:"poke"`
IgnoreUserAgent StringOrStringSlice `yaml:"ignoreUserAgent" json:"ignoreUserAgent"`
}

Expand All @@ -75,6 +79,7 @@ func CreateConfig() *Config {
splittedNames: []string{},
Dynamic: nil,
Blocking: nil,
Poke: nil,
IgnoreUserAgent: StringOrStringSlice{},
}
}
Expand All @@ -98,16 +103,30 @@ func (c *Config) BuildRequest(middlewareName string) (*http.Request, error) {
return nil, fmt.Errorf("you must specify at least one name or a group")
}

if c.Dynamic != nil && c.Blocking != nil {
return nil, fmt.Errorf("only supply one strategy: dynamic or blocking")
strategyCount := 0
if c.Dynamic != nil {
strategyCount++
}
if c.Blocking != nil {
strategyCount++
}
if c.Poke != nil {
strategyCount++
}
if strategyCount > 1 {
return nil, fmt.Errorf("only supply one strategy")
}

if c.Dynamic != nil {
switch {
case c.Dynamic != nil:
return c.buildDynamicRequest(middlewareName)
} else if c.Blocking != nil {
case c.Blocking != nil:
return c.buildBlockingRequest()
case c.Poke != nil:
return c.buildPokeRequest()
default:
return nil, fmt.Errorf("no strategy configured")
}
return nil, fmt.Errorf("no strategy configured")
}

func (c *Config) buildDynamicRequest(middlewareName string) (*http.Request, error) {
Expand Down Expand Up @@ -214,3 +233,36 @@ func (c *Config) buildBlockingRequest() (*http.Request, error) {

return request, nil
}

func (c *Config) buildPokeRequest() (*http.Request, error) {
if c.Poke == nil {
return nil, fmt.Errorf("poke config is nil")
}

request, err := http.NewRequest("GET", fmt.Sprintf("%s/api/strategies/poke", c.SablierURL), nil)
if err != nil {
return nil, err
}

q := request.URL.Query()

if c.SessionDuration != "" {
_, err = time.ParseDuration(c.SessionDuration)
if err != nil {
return nil, fmt.Errorf("error parsing poke.sessionDuration: %v", err)
}
q.Add("session_duration", c.SessionDuration)
}

for _, name := range c.splittedNames {
q.Add("names", name)
}

if c.Group != "" {
q.Add("group", c.Group)
}

request.URL.RawQuery = q.Encode()

return request, nil
}
13 changes: 13 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,25 @@ func TestConfig_BuildRequest(t *testing.T) {
SessionDuration string
Dynamic *traefik.DynamicConfiguration
Blocking *traefik.BlockingConfiguration
Poke *traefik.PokeConfiguration
}
tests := []struct {
name string
fields fields
want *http.Request
wantErr bool
}{
{
name: "poke session with group",
fields: fields{
SablierURL: "http://sablier:10000",
Group: "default",
SessionDuration: "1m",
Poke: &traefik.PokeConfiguration{},
},
want: createRequest("GET", "http://sablier:10000/api/strategies/poke?group=default&session_duration=1m", nil),
wantErr: false,
},
{
name: "dynamic session with required values",
fields: fields{
Expand Down Expand Up @@ -294,6 +306,7 @@ func TestConfig_BuildRequest(t *testing.T) {
SessionDuration: tt.fields.SessionDuration,
Dynamic: tt.fields.Dynamic,
Blocking: tt.fields.Blocking,
Poke: tt.fields.Poke,
}

got, err := c.BuildRequest("sablier-middleware")
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type SablierMiddleware struct {
failOpen bool
ignoreUserAgents []*regexp.Regexp
keepAliveInterval time.Duration
isPoke bool
}

// New function creates the configuration
Expand Down Expand Up @@ -57,6 +58,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
failOpen: config.FailOpen,
ignoreUserAgents: ignoreUserAgents,
keepAliveInterval: keepAliveInterval,
isPoke: config.Poke != nil,
}, nil
}

Expand Down Expand Up @@ -108,7 +110,7 @@ func (sm *SablierMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request

useRedirect := false

if resp.Header.Get("X-Sablier-Session-Status") == "ready" {
if resp.Header.Get("X-Sablier-Session-Status") == "ready" || sm.isPoke {
// Check if the backend already received request data
if sm.keepAliveInterval > 0 {
go sm.keepAlive(req.Context())
Expand Down