From 562bb0b474cf52cfccce722dbb008dc7a2f2210b Mon Sep 17 00:00:00 2001 From: Gordon Bleux <33967640+UiP9AV6Y@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:43:00 +0100 Subject: [PATCH] HTTP probe: add support for HTTP methods, request payload and status validation this extends the HTTP probe with additional properties related to the outgoing HTTP request. additionally a response status validation can be configured which is matched against the status line. also the initialization has been refactored to error out on invalid duration definitions instead of failing the probe execution. --- README.md | 8 ++++ internal/config/types.go | 12 ++++-- pkg/probe/probe_http.go | 82 +++++++++++++++++++++++++--------------- pkg/probe/server.go | 46 ++++++++++++---------- 4 files changed, 93 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index ae2bce6..c7ff87a 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ probe "probe-name" { } http { + method = "post" scheme = "http" host = { hostname = "localhost" @@ -296,6 +297,13 @@ probe "probe-name" { } path = "/status" timeout = "5s" + payload = "{\"body\": 123}" + # regex to match against the response status line + # (e.g. 403 Forbidden) + expectStatus = "(200|201)" + headers = { + Content-Type = "application/json" + } } } ``` diff --git a/internal/config/types.go b/internal/config/types.go index e4c8386..7c95fb3 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -43,11 +43,15 @@ type SMTP struct { Host } -type HttpGet struct { +type HTTP struct { + Method string Scheme string Host - Path string - Timeout string + Path string + Timeout string + Payload string + ExpectStatus string + Headers map[string]string } type Probe struct { @@ -58,7 +62,7 @@ type Probe struct { Redis *Redis MongoDB *MongoDB Amqp *Amqp - HTTP *HttpGet + HTTP *HTTP SMTP *SMTP } diff --git a/pkg/probe/probe_http.go b/pkg/probe/probe_http.go index ff01e37..4ee8e61 100644 --- a/pkg/probe/probe_http.go +++ b/pkg/probe/probe_http.go @@ -2,8 +2,11 @@ package probe import ( "fmt" + "net" "net/http" "net/url" + "regexp" + "strings" "time" "github.com/mittwald/mittnite/internal/config" @@ -11,69 +14,86 @@ import ( log "github.com/sirupsen/logrus" ) -type httpGetProbe struct { +type httpProbe struct { + method string scheme string host string path string - timeout string + payload string + headers map[string]string + timeout time.Duration + status *regexp.Regexp } -func NewHttpProbe(cfg *config.HttpGet) *httpGetProbe { - cfg.Scheme = helper.ResolveEnv(cfg.Scheme) +func NewHttpProbe(cfg *config.HTTP) (*httpProbe, error) { + cfg.Method = helper.SetDefaultStringIfEmpty(helper.ResolveEnv(cfg.Method), "GET", "method", "http") + cfg.Scheme = helper.SetDefaultStringIfEmpty(helper.ResolveEnv(cfg.Scheme), "http", "scheme", "http") cfg.Hostname = helper.ResolveEnv(cfg.Hostname) cfg.Port = helper.ResolveEnv(cfg.Port) cfg.Path = helper.ResolveEnv(cfg.Path) - cfg.Timeout = helper.ResolveEnv(cfg.Timeout) - - if cfg.Scheme == "" { - cfg.Scheme = "http" - } + cfg.Timeout = helper.SetDefaultStringIfEmpty(helper.ResolveEnv(cfg.Timeout), "5s", "timeout", "http") + cfg.ExpectStatus = helper.SetDefaultStringIfEmpty(helper.ResolveEnv(cfg.ExpectStatus), `(1|2|3)\d\d\s`, "expectStatus", "http") + method := strings.ToUpper(cfg.Method) host := cfg.Hostname if cfg.Port != "" { - host = fmt.Sprintf("%s:%s", cfg.Hostname, cfg.Port) + host = net.JoinHostPort(cfg.Hostname, cfg.Port) + } + + status, err := regexp.Compile(cfg.ExpectStatus) + if err != nil { + return nil, fmt.Errorf("invalid HTTP status line regexp: %w", err) } - connCfg := httpGetProbe{ + timeout, err := time.ParseDuration(cfg.Timeout) + if err != nil { + return nil, fmt.Errorf("invalid timeout duration: %w", err) + } + + connCfg := &httpProbe{ + method: method, scheme: cfg.Scheme, host: host, path: cfg.Path, - timeout: cfg.Timeout, + status: status, + timeout: timeout, + payload: cfg.Payload, + headers: cfg.Headers, } - return &connCfg + return connCfg, nil } -func (h *httpGetProbe) Exec() error { - timeout := time.Second * 5 - if h.timeout != "" { - duration, err := time.ParseDuration(h.timeout) - if err == nil { - timeout = duration - } else { - return fmt.Errorf("invalid timeout duration: %s", err) - } - } - +func (h *httpProbe) Exec() error { u := url.URL{ Scheme: h.scheme, Host: h.host, Path: h.path, } urlStr := u.String() - client := &http.Client{ - Timeout: timeout, + Timeout: h.timeout, } - res, err := client.Get(urlStr) + + data := strings.NewReader(h.payload) + req, err := http.NewRequest(h.method, u.String(), data) + if err != nil { + return err + } + + for k, v := range h.headers { + req.Header.Set(k, v) + } + + res, err := client.Do(req) if err != nil { return err } - if res.StatusCode >= 200 && res.StatusCode < 400 { - log.WithFields(log.Fields{"kind": "probe", "name": "http", "status": "alive", "host": urlStr}).Debug() - return nil + if !h.status.MatchString(res.Status) { + return fmt.Errorf("http service %q returned status %q", urlStr, res.Status) } - return fmt.Errorf("http service '%s' returned status code %d", urlStr, res.StatusCode) + log.WithFields(log.Fields{"kind": "probe", "name": "http", "status": "alive", "host": urlStr}).Debug() + return nil } diff --git a/pkg/probe/server.go b/pkg/probe/server.go index d4d9ca0..e2cbdf3 100644 --- a/pkg/probe/server.go +++ b/pkg/probe/server.go @@ -143,29 +143,15 @@ func filterWaitProbes(cfg *config.Ignition, probes map[string]Probe) map[string] } func buildProbesFromConfig(cfg *config.Ignition) (map[string]Probe, error) { - result := make(map[string]Probe) - var errs []error + result := make(map[string]Probe) for i := range cfg.Probes { - if cfg.Probes[i].Filesystem != "" { - result[cfg.Probes[i].Name] = &filesystemProbe{cfg.Probes[i].Filesystem} - } else if cfg.Probes[i].MySQL != nil { - result[cfg.Probes[i].Name] = NewMySQLProbe(cfg.Probes[i].MySQL) - } else if cfg.Probes[i].Redis != nil { - result[cfg.Probes[i].Name] = NewRedisProbe(cfg.Probes[i].Redis) - } else if cfg.Probes[i].MongoDB != nil { - var err error - result[cfg.Probes[i].Name], err = NewMongoDBProbe(cfg.Probes[i].MongoDB) - if err != nil { - errs = append(errs, err) - } - } else if cfg.Probes[i].Amqp != nil { - result[cfg.Probes[i].Name] = NewAmqpProbe(cfg.Probes[i].Amqp) - } else if cfg.Probes[i].HTTP != nil { - result[cfg.Probes[i].Name] = NewHttpProbe(cfg.Probes[i].HTTP) - } else if cfg.Probes[i].SMTP != nil { - result[cfg.Probes[i].Name] = NewSmtpProbe(cfg.Probes[i].SMTP) + p, err := newProbe(cfg.Probes[i]) + if err != nil { + errs = append(errs, err) + } else if p != nil { + result[cfg.Probes[i].Name] = p } } @@ -176,3 +162,23 @@ func buildProbesFromConfig(cfg *config.Ignition) (map[string]Probe, error) { return result, err } + +func newProbe(p config.Probe) (Probe, error) { + if p.Filesystem != "" { + return &filesystemProbe{p.Filesystem}, nil + } else if p.MySQL != nil { + return NewMySQLProbe(p.MySQL), nil + } else if p.Redis != nil { + return NewRedisProbe(p.Redis), nil + } else if p.MongoDB != nil { + return NewMongoDBProbe(p.MongoDB) + } else if p.Amqp != nil { + return NewAmqpProbe(p.Amqp), nil + } else if p.HTTP != nil { + return NewHttpProbe(p.HTTP) + } else if p.SMTP != nil { + return NewSmtpProbe(p.SMTP), nil + } + + return nil, nil +}