ipt-loadbalancer/pkg/healthcheck/http/http.go
2023-11-14 00:40:07 +01:00

128 lines
3.9 KiB
Go

// Package http contains a http health-check
package http
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"git.luzifer.io/luzifer/ipt-loadbalancer/pkg/config"
"git.luzifer.io/luzifer/ipt-loadbalancer/pkg/healthcheck/common"
"github.com/Luzifer/go_helpers/v2/fieldcollection"
)
const (
settingCode = "code"
settingExpectContent = "expectContent"
settingHost = "host"
settingInsecureTLS = "insecureTLS"
settingMethod = "method"
settingPath = "path"
settingPort = "port"
settingTimeout = "timeout"
settingTLS = "tls"
)
type (
// Check represents the HTTP check
Check struct{}
)
var (
defCode = http.StatusOK
defExpectContent = ""
defHost = ""
defInsecureTLS = false
defMethod = http.MethodGet
defPath = "/"
defTimeout = time.Second
defTLS = false
)
// New returns a new HTTP check
func New() Check { return Check{} }
// Check executes the check
func (c Check) Check(settings *fieldcollection.FieldCollection, target config.Target) error {
ctx, cancel := context.WithTimeout(context.Background(), settings.MustDuration(settingTimeout, &defTimeout))
defer cancel()
u := url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:%d", target.Addr, settings.MustInt64(settingPort, c.intToInt64Ptr(target.Port))),
Path: settings.MustString(settingPath, &defPath),
}
if settings.MustBool(settingTLS, &defTLS) {
u.Scheme = "https"
}
req, err := http.NewRequestWithContext(ctx, settings.MustString(settingMethod, &defMethod), u.String(), nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("User-Agent", "ipt-loadbalancer/v1 (https://git.luzifer.io/luzifer/ipt-loadbalancer)")
if hh := settings.MustString(settingHost, &defHost); hh != defHost {
req.Header.Set("Host", hh)
}
client := http.Client{}
if settings.MustBool(settingInsecureTLS, &defInsecureTLS) {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // The intention is to use insecure TLS
},
}
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != int(settings.MustInt64(settingCode, c.intToInt64Ptr(defCode))) {
return fmt.Errorf("unexpected status code %d != %d", resp.StatusCode, settings.MustInt64(settingCode, c.intToInt64Ptr(defCode)))
}
if settings.MustString(settingExpectContent, &defExpectContent) == defExpectContent {
return nil
}
content, err := io.ReadAll(req.Body)
if err != nil {
return fmt.Errorf("reading response body: %w", err)
}
if !strings.Contains(string(content), settings.MustString(settingExpectContent, &defExpectContent)) {
return fmt.Errorf("expected content not found in body")
}
return nil
}
// Help returns the set of settings used in the check
func (Check) Help() (help []common.SettingHelp) {
return []common.SettingHelp{
{Name: settingCode, Default: defCode, Description: "HTTP Status-Code to expect from the request"},
{Name: settingExpectContent, Default: defExpectContent, Description: "Content to search in the response body"},
{Name: settingHost, Default: defHost, Description: "Host header to send with the request"},
{Name: settingInsecureTLS, Default: defInsecureTLS, Description: "Skip TLS certificate validation"},
{Name: settingMethod, Default: defMethod, Description: "Method to use for request"},
{Name: settingPath, Default: defPath, Description: "Path to send the request to"},
{Name: settingPort, Default: "target-port", Description: "Port to send the request to"},
{Name: settingTimeout, Default: defTimeout, Description: "Timeout for the HTTP request"},
{Name: settingTLS, Default: defTLS, Description: "Connect to port using TLS"},
}
}
func (Check) intToInt64Ptr(i int) *int64 {
i64 := int64(i)
return &i64
}