diff --git a/README.md b/README.md index 91ce11a..550fb47 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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" @@ -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: {} ``` diff --git a/config.go b/config.go index 0f19af6..e455ac2 100644 --- a/config.go +++ b/config.go @@ -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 @@ -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"` } @@ -75,6 +79,7 @@ func CreateConfig() *Config { splittedNames: []string{}, Dynamic: nil, Blocking: nil, + Poke: nil, IgnoreUserAgent: StringOrStringSlice{}, } } @@ -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) { @@ -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 +} diff --git a/config_test.go b/config_test.go index 3c11d95..7e5ae97 100644 --- a/config_test.go +++ b/config_test.go @@ -21,6 +21,7 @@ func TestConfig_BuildRequest(t *testing.T) { SessionDuration string Dynamic *traefik.DynamicConfiguration Blocking *traefik.BlockingConfiguration + Poke *traefik.PokeConfiguration } tests := []struct { name string @@ -28,6 +29,17 @@ func TestConfig_BuildRequest(t *testing.T) { 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{ @@ -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") diff --git a/main.go b/main.go index dfb50a9..b6ec6b3 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ type SablierMiddleware struct { failOpen bool ignoreUserAgents []*regexp.Regexp keepAliveInterval time.Duration + isPoke bool } // New function creates the configuration @@ -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 } @@ -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())