#126: Add client configuration

This commit is contained in:
TwinProduction 2021-07-28 21:41:26 -04:00 committed by Chris
parent be4e9aba1e
commit 9cd6355056
11 changed files with 359 additions and 114 deletions

View File

@ -33,6 +33,8 @@ For more details, see [Usage](#usage)
- [Conditions](#conditions) - [Conditions](#conditions)
- [Placeholders](#placeholders) - [Placeholders](#placeholders)
- [Functions](#functions) - [Functions](#functions)
- [Storage](#storage)
- [Client configuration](#client-configuration)
- [Alerting](#alerting) - [Alerting](#alerting)
- [Configuring Slack alerts](#configuring-slack-alerts) - [Configuring Slack alerts](#configuring-slack-alerts)
- [Configuring Discord alerts](#configuring-discord-alerts) - [Configuring Discord alerts](#configuring-discord-alerts)
@ -142,7 +144,6 @@ If you want to test it locally, see [Docker](#docker).
| `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` | | `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` |
| `services[].url` | URL to send the request to. | Required `""` | | `services[].url` | URL to send the request to. | Required `""` |
| `services[].method` | Request method. | `GET` | | `services[].method` | Request method. | `GET` |
| `services[].insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
| `services[].conditions` | Conditions used to determine the health of the service. See [Conditions](#conditions). | `[]` | | `services[].conditions` | Conditions used to determine the health of the service. See [Conditions](#conditions). | `[]` |
| `services[].interval` | Duration to wait between every status check. | `60s` | | `services[].interval` | Duration to wait between every status check. | `60s` |
| `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` | | `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
@ -157,6 +158,7 @@ If you want to test it locally, see [Docker](#docker).
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` | | `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` | | `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` | | `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
| `services[].client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` | | `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` |
| `security` | Security configuration. | `{}` | | `security` | Security configuration. | `{}` |
| `security.basic` | Basic authentication security configuration. | `{}` | | `security.basic` | Basic authentication security configuration. | `{}` |
@ -238,6 +240,42 @@ storage:
See [examples/docker-compose-sqlite-storage](examples/docker-compose-sqlite-storage) for an example. See [examples/docker-compose-sqlite-storage](examples/docker-compose-sqlite-storage) for an example.
### Client configuration
In order to support a wide range of environments, each monitored service has a unique configuration for
the client used to send the request.
| Parameter | Description | Default |
|:-------------------------|:----------------------------------------------------------------------------- |:-------------- |
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
| `client.ignore-follow` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
| `client.timeout` | Duration before timing out. | `10s` |
Note that some of these parameters are ignored based on the type of service. For instance, there's no certificate involved
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for a service of that type will not do anything.
This default configuration is as follows:
```yaml
client:
insecure: false
ignore-follow: false
timeout: 10s
```
Note that this configuration is only available under `services[]`, `alerting.mattermost` and `alerting.custom`.
Here's an example with the client configuration under `service[]`:
```yaml
services:
- name: twinnation
url: "https://twinnation.org/health"
client:
insecure: false
ignore-follow: false
timeout: 10s
conditions:
- "[STATUS] == 200"
```
### Alerting ### Alerting
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
individual services with configurable descriptions and thresholds. individual services with configurable descriptions and thresholds.
@ -260,7 +298,7 @@ ignored.
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` | | `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` | | `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` | | `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
| `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` | | `alerting.mattermost.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` | | `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` | | `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
| `alerting.messagebird.originator` | The sender of the message | Required `""` | | `alerting.messagebird.originator` | The sender of the message | Required `""` |
@ -271,9 +309,9 @@ ignored.
| `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` | | `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` |
| `alerting.custom.url` | Custom alerting request url | Required `""` | | `alerting.custom.url` | Custom alerting request url | Required `""` |
| `alerting.custom.method` | Request method | `GET` | | `alerting.custom.method` | Request method | `GET` |
| `alerting.custom.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
| `alerting.custom.body` | Custom alerting request body. | `""` | | `alerting.custom.body` | Custom alerting request body. | `""` |
| `alerting.custom.headers` | Custom alerting request headers | `{}` | | `alerting.custom.headers` | Custom alerting request headers | `{}` |
| `alerting.custom.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A | | `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A | | `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A | | `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
@ -394,7 +432,8 @@ services:
alerting: alerting:
mattermost: mattermost:
webhook-url: "http://**********/hooks/**********" webhook-url: "http://**********/hooks/**********"
insecure: true client:
insecure: true
services: services:
- name: twinnation - name: twinnation
@ -490,7 +529,6 @@ alerting:
custom: custom:
url: "https://hooks.slack.com/services/**********/**********/**********" url: "https://hooks.slack.com/services/**********/**********/**********"
method: "POST" method: "POST"
insecure: true
body: | body: |
{ {
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]" "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]"
@ -739,7 +777,7 @@ such as 1ms. You'll notice that the response time does not fluctuate - that is b
different goroutines, there's a global lock that prevents multiple services from running at the same time. different goroutines, there's a global lock that prevents multiple services from running at the same time.
Unfortunately, there is a drawback. If you have a lot of services, including some that are very slow or prone to time out (the default Unfortunately, there is a drawback. If you have a lot of services, including some that are very slow or prone to time out (the default
time out is 10s for HTTP and 5s for TCP), then it means that for the entire duration of the request, no other services can be evaluated. timeout is 10s), then it means that for the entire duration of the request, no other services can be evaluated.
**This does mean that Gatus will be unable to evaluate the health of other services**. **This does mean that Gatus will be unable to evaluate the health of other services**.
The interval does not include the duration of the request itself, which means that if a service has an interval of 30s The interval does not include the duration of the request itself, which means that if a service has an interval of 30s
@ -762,11 +800,13 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
| Protocol | Timeout | | Protocol | Timeout |
|:-------- |:------- | |:-------- |:------- |
| HTTP | 10s | HTTP | 10s
| TCP | 5s | TCP | 10s
| ICMP | 10s
To modify the timeout, see [Client configuration](#client-configuration).
### Monitoring a TCP service ### Monitoring a TCP service
By prefixing `services[].url` with `tcp:\\`, you can monitor TCP services at a very basic level: By prefixing `services[].url` with `tcp:\\`, you can monitor TCP services at a very basic level:
```yaml ```yaml
@ -1006,4 +1046,3 @@ No such header is required to query the API.
You can find the full list of sponsors [here](https://github.com/sponsors/TwinProduction). You can find the full list of sponsors [here](https://github.com/sponsors/TwinProduction).
[<img src="https://github.com/math280h.png" width="35" />](https://github.com/math280h) [<img src="https://github.com/math280h.png" width="35" />](https://github.com/math280h)
[<img src="https://github.com/mateothegreat.png" width="35" />](https://github.com/mateothegreat)

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -19,18 +20,29 @@ import (
type AlertProvider struct { type AlertProvider struct {
URL string `yaml:"url"` URL string `yaml:"url"`
Method string `yaml:"method,omitempty"` Method string `yaml:"method,omitempty"`
Insecure bool `yaml:"insecure,omitempty"` Insecure bool `yaml:"insecure,omitempty"` // deprecated
Body string `yaml:"body,omitempty"` Body string `yaml:"body,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"` Headers map[string]string `yaml:"headers,omitempty"`
Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"` Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"`
// ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client"`
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type // DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert"` DefaultAlert *alert.Alert `yaml:"default-alert"`
} }
// IsValid returns whether the provider's configuration is valid // IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool { func (provider *AlertProvider) IsValid() bool {
return len(provider.URL) > 0 if provider.ClientConfig == nil {
provider.ClientConfig = client.GetDefaultConfig()
// XXX: remove the next 3 lines in v3.0.0
if provider.Insecure {
log.Println("WARNING: alerting.*.insecure has been deprecated and will be removed in v3.0.0 in favor of alerting.*.client.insecure")
provider.ClientConfig.Insecure = true
}
}
return len(provider.URL) > 0 && provider.ClientConfig != nil
} }
// ToCustomAlertProvider converts the provider into a custom.AlertProvider // ToCustomAlertProvider converts the provider into a custom.AlertProvider
@ -103,7 +115,7 @@ func (provider *AlertProvider) Send(serviceName, alertDescription string, resolv
return []byte("{}"), nil return []byte("{}"), nil
} }
request := provider.buildHTTPRequest(serviceName, alertDescription, resolved) request := provider.buildHTTPRequest(serviceName, alertDescription, resolved)
response, err := client.GetHTTPClient(provider.Insecure).Do(request) response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,17 +2,22 @@ package mattermost
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"github.com/TwinProduction/gatus/alerting/alert" "github.com/TwinProduction/gatus/alerting/alert"
"github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/alerting/provider/custom"
"github.com/TwinProduction/gatus/client"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
) )
// AlertProvider is the configuration necessary for sending an alert using Mattermost // AlertProvider is the configuration necessary for sending an alert using Mattermost
type AlertProvider struct { type AlertProvider struct {
WebhookURL string `yaml:"webhook-url"` WebhookURL string `yaml:"webhook-url"`
Insecure bool `yaml:"insecure,omitempty"` Insecure bool `yaml:"insecure,omitempty"` // deprecated
// ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client"`
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type // DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert"` DefaultAlert *alert.Alert `yaml:"default-alert"`
@ -20,6 +25,14 @@ type AlertProvider struct {
// IsValid returns whether the provider's configuration is valid // IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool { func (provider *AlertProvider) IsValid() bool {
if provider.ClientConfig == nil {
provider.ClientConfig = client.GetDefaultConfig()
// XXX: remove the next 3 lines in v3.0.0
if provider.Insecure {
log.Println("WARNING: alerting.mattermost.insecure has been deprecated and will be removed in v3.0.0 in favor of alerting.mattermost.client.insecure")
provider.ClientConfig.Insecure = true
}
}
return len(provider.WebhookURL) > 0 return len(provider.WebhookURL) > 0
} }
@ -45,9 +58,9 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition) results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
} }
return &custom.AlertProvider{ return &custom.AlertProvider{
URL: provider.WebhookURL, URL: provider.WebhookURL,
Method: http.MethodPost, Method: http.MethodPost,
Insecure: provider.Insecure, ClientConfig: provider.ClientConfig,
Body: fmt.Sprintf(`{ Body: fmt.Sprintf(`{
"text": "", "text": "",
"username": "gatus", "username": "gatus",

View File

@ -14,58 +14,14 @@ import (
"github.com/go-ping/ping" "github.com/go-ping/ping"
) )
var (
secureHTTPClient *http.Client
insecureHTTPClient *http.Client
// pingTimeout is the timeout for the Ping function
// This is mainly exposed for testing purposes
pingTimeout = 5 * time.Second
// httpTimeout is the timeout for secureHTTPClient and insecureHTTPClient
httpTimeout = 10 * time.Second
)
// GetHTTPClient returns the shared HTTP client // GetHTTPClient returns the shared HTTP client
func GetHTTPClient(insecure bool) *http.Client { func GetHTTPClient(config *Config) *http.Client {
if insecure { return config.GetHTTPClient()
if insecureHTTPClient == nil {
insecureHTTPClient = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // Don't follow redirects
},
}
}
return insecureHTTPClient
}
if secureHTTPClient == nil {
secureHTTPClient = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
Proxy: http.ProxyFromEnvironment,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // Don't follow redirects
},
}
}
return secureHTTPClient
} }
// CanCreateTCPConnection checks whether a connection can be established with a TCP service // CanCreateTCPConnection checks whether a connection can be established with a TCP service
func CanCreateTCPConnection(address string) bool { func CanCreateTCPConnection(address string, config *Config) bool {
conn, err := net.DialTimeout("tcp", address, 5*time.Second) conn, err := net.DialTimeout("tcp", address, config.Timeout)
if err != nil { if err != nil {
return false return false
} }
@ -74,7 +30,7 @@ func CanCreateTCPConnection(address string) bool {
} }
// CanPerformStartTLS checks whether a connection can be established to an address using the STARTTLS protocol // CanPerformStartTLS checks whether a connection can be established to an address using the STARTTLS protocol
func CanPerformStartTLS(address string, insecure bool) (connected bool, certificate *x509.Certificate, err error) { func CanPerformStartTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) {
hostAndPort := strings.Split(address, ":") hostAndPort := strings.Split(address, ":")
if len(hostAndPort) != 2 { if len(hostAndPort) != 2 {
return false, nil, errors.New("invalid address for starttls, format must be host:port") return false, nil, errors.New("invalid address for starttls, format must be host:port")
@ -84,7 +40,7 @@ func CanPerformStartTLS(address string, insecure bool) (connected bool, certific
return return
} }
err = smtpClient.StartTLS(&tls.Config{ err = smtpClient.StartTLS(&tls.Config{
InsecureSkipVerify: insecure, InsecureSkipVerify: config.Insecure,
ServerName: hostAndPort[0], ServerName: hostAndPort[0],
}) })
if err != nil { if err != nil {
@ -101,13 +57,13 @@ func CanPerformStartTLS(address string, insecure bool) (connected bool, certific
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged // Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
// //
// Note that this function takes at least 100ms, even if the address is 127.0.0.1 // Note that this function takes at least 100ms, even if the address is 127.0.0.1
func Ping(address string) (bool, time.Duration) { func Ping(address string, config *Config) (bool, time.Duration) {
pinger, err := ping.NewPinger(address) pinger, err := ping.NewPinger(address)
if err != nil { if err != nil {
return false, 0 return false, 0
} }
pinger.Count = 1 pinger.Count = 1
pinger.Timeout = pingTimeout pinger.Timeout = config.Timeout
// Set the pinger's privileged mode to true for every operating system except darwin // Set the pinger's privileged mode to true for every operating system except darwin
// https://github.com/TwinProduction/gatus/issues/132 // https://github.com/TwinProduction/gatus/issues/132
pinger.SetPrivileged(runtime.GOOS != "darwin") pinger.SetPrivileged(runtime.GOOS != "darwin")

View File

@ -6,43 +6,28 @@ import (
) )
func TestGetHTTPClient(t *testing.T) { func TestGetHTTPClient(t *testing.T) {
if secureHTTPClient != nil { GetHTTPClient(&Config{
t.Error("secureHTTPClient should've been nil since it hasn't been called a single time yet") Insecure: false,
} IgnoreRedirect: false,
if insecureHTTPClient != nil { Timeout: 0,
t.Error("insecureHTTPClient should've been nil since it hasn't been called a single time yet") httpClient: nil,
} })
_ = GetHTTPClient(false)
if secureHTTPClient == nil {
t.Error("secureHTTPClient shouldn't have been nil, since it has been called once")
}
if insecureHTTPClient != nil {
t.Error("insecureHTTPClient should've been nil since it hasn't been called a single time yet")
}
_ = GetHTTPClient(true)
if secureHTTPClient == nil {
t.Error("secureHTTPClient shouldn't have been nil, since it has been called once")
}
if insecureHTTPClient == nil {
t.Error("insecureHTTPClient shouldn't have been nil, since it has been called once")
}
} }
func TestPing(t *testing.T) { func TestPing(t *testing.T) {
pingTimeout = 500 * time.Millisecond if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond}); !success {
if success, rtt := Ping("127.0.0.1"); !success {
t.Error("expected true") t.Error("expected true")
if rtt == 0 { if rtt == 0 {
t.Error("Round-trip time returned on success should've higher than 0") t.Error("Round-trip time returned on success should've higher than 0")
} }
} }
if success, rtt := Ping("256.256.256.256"); success { if success, rtt := Ping("256.256.256.256", &Config{Timeout: 500 * time.Millisecond}); success {
t.Error("expected false, because the IP is invalid") t.Error("expected false, because the IP is invalid")
if rtt != 0 { if rtt != 0 {
t.Error("Round-trip time returned on failure should've been 0") t.Error("Round-trip time returned on failure should've been 0")
} }
} }
if success, rtt := Ping("192.168.152.153"); success { if success, rtt := Ping("192.168.152.153", &Config{Timeout: 500 * time.Millisecond}); success {
t.Error("expected false, because the IP is valid but the host should be unreachable") t.Error("expected false, because the IP is valid but the host should be unreachable")
if rtt != 0 { if rtt != 0 {
t.Error("Round-trip time returned on failure should've been 0") t.Error("Round-trip time returned on failure should've been 0")
@ -88,7 +73,7 @@ func TestCanPerformStartTLS(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
connected, _, err := CanPerformStartTLS(tt.args.address, tt.args.insecure) connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr) t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
return return
@ -101,7 +86,7 @@ func TestCanPerformStartTLS(t *testing.T) {
} }
func TestCanCreateTCPConnection(t *testing.T) { func TestCanCreateTCPConnection(t *testing.T) {
if CanCreateTCPConnection("127.0.0.1") { if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
t.Error("should've failed, because there's no port in the address") t.Error("should've failed, because there's no port in the address")
} }
} }

73
client/config.go Normal file
View File

@ -0,0 +1,73 @@
package client
import (
"crypto/tls"
"net/http"
"time"
)
const (
defaultHTTPTimeout = 10 * time.Second
)
var (
// DefaultConfig is the default client configuration
defaultConfig = Config{
Insecure: false,
IgnoreRedirect: false,
Timeout: defaultHTTPTimeout,
}
)
// GetDefaultConfig returns a copy of the default configuration
func GetDefaultConfig() *Config {
cfg := defaultConfig
return &cfg
}
// Config is the configuration for clients
type Config struct {
// Insecure determines whether to skip verifying the server's certificate chain and host name
Insecure bool `yaml:"insecure"`
// IgnoreRedirect determines whether to ignore redirects (true) or follow them (false, default)
IgnoreRedirect bool `yaml:"ignore-redirect"`
// Timeout for the client
Timeout time.Duration `yaml:"timeout"`
httpClient *http.Client
}
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
func (c *Config) ValidateAndSetDefaults() {
if c.Timeout < time.Millisecond {
c.Timeout = 10 * time.Second
}
}
// GetHTTPClient return a HTTP client matching the Config's parameters.
func (c *Config) GetHTTPClient() *http.Client {
if c.httpClient == nil {
c.httpClient = &http.Client{
Timeout: c.Timeout,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.Insecure,
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if c.IgnoreRedirect {
// Don't follow redirects
return http.ErrUseLastResponse
}
// Follow redirects
return nil
},
}
}
return c.httpClient
}

37
client/config_test.go Normal file
View File

@ -0,0 +1,37 @@
package client
import (
"net/http"
"testing"
"time"
)
func TestConfig_GetHTTPClient(t *testing.T) {
insecureConfig := &Config{Insecure: true}
insecureConfig.ValidateAndSetDefaults()
insecureClient := insecureConfig.GetHTTPClient()
if !(insecureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Error("expected Config.Insecure set to true to cause the HTTP client to skip certificate verification")
}
if insecureClient.Timeout != defaultHTTPTimeout {
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
}
request, _ := http.NewRequest("GET", "", nil)
if err := insecureClient.CheckRedirect(request, nil); err != nil {
t.Error("expected Config.IgnoreRedirect set to false to cause the HTTP client's CheckRedirect to return nil")
}
secureConfig := &Config{IgnoreRedirect: true, Timeout: 5 * time.Second}
secureConfig.ValidateAndSetDefaults()
secureClient := secureConfig.GetHTTPClient()
if (secureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Error("expected Config.Insecure set to false to cause the HTTP client to not skip certificate verification")
}
if secureClient.Timeout != 5*time.Second {
t.Error("expected Config.Timeout to cause the HTTP client to have a timeout of 5s")
}
request, _ = http.NewRequest("GET", "", nil)
if err := secureClient.CheckRedirect(request, nil); err != http.ErrUseLastResponse {
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
}
}

View File

@ -16,6 +16,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/slack" "github.com/TwinProduction/gatus/alerting/provider/slack"
"github.com/TwinProduction/gatus/alerting/provider/telegram" "github.com/TwinProduction/gatus/alerting/provider/telegram"
"github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/client"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/k8stest" "github.com/TwinProduction/gatus/k8stest"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -40,17 +41,31 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
storage: storage:
file: %s file: %s
services: services:
- name: twinnation - name: twinnation
url: https://twinnation.org/health url: https://twinnation.org/health
interval: 15s interval: 15s
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
- name: github - name: github
url: https://api.github.com/healthz url: https://api.github.com/healthz
client:
insecure: true
ignore-redirect: true
timeout: 5s
conditions: conditions:
- "[STATUS] != 400" - "[STATUS] != 400"
- "[STATUS] != 500" - "[STATUS] != 500"
- name: example
url: https://example.com/
interval: 30m
client:
insecure: true
conditions:
- "[STATUS] == 200"
`, file))) `, file)))
if err != nil { if err != nil {
t.Error("expected no error, got", err.Error()) t.Error("expected no error, got", err.Error())
@ -58,33 +73,75 @@ services:
if config == nil { if config == nil {
t.Fatal("Config shouldn't have been nil") t.Fatal("Config shouldn't have been nil")
} }
if len(config.Services) != 2 { if len(config.Services) != 3 {
t.Error("Should have returned two services") t.Error("Should have returned two services")
} }
if config.Services[0].URL != "https://twinnation.org/health" { if config.Services[0].URL != "https://twinnation.org/health" {
t.Errorf("URL should have been %s", "https://twinnation.org/health") t.Errorf("URL should have been %s", "https://twinnation.org/health")
} }
if config.Services[1].URL != "https://api.github.com/healthz" {
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
}
if config.Services[0].Method != "GET" { if config.Services[0].Method != "GET" {
t.Errorf("Method should have been %s (default)", "GET") t.Errorf("Method should have been %s (default)", "GET")
} }
if config.Services[1].Method != "GET" {
t.Errorf("Method should have been %s (default)", "GET")
}
if config.Services[0].Interval != 15*time.Second { if config.Services[0].Interval != 15*time.Second {
t.Errorf("Interval should have been %s", 15*time.Second) t.Errorf("Interval should have been %s", 15*time.Second)
} }
if config.Services[1].Interval != 60*time.Second { if config.Services[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[0].ClientConfig.Insecure)
}
if config.Services[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Services[0].ClientConfig.IgnoreRedirect)
}
if config.Services[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
t.Errorf("ClientConfig.Timeout should have been %v, got %v", client.GetDefaultConfig().Timeout, config.Services[0].ClientConfig.Timeout)
} }
if len(config.Services[0].Conditions) != 1 { if len(config.Services[0].Conditions) != 1 {
t.Errorf("There should have been %d conditions", 1) t.Errorf("There should have been %d conditions", 1)
} }
if config.Services[1].URL != "https://api.github.com/healthz" {
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
}
if config.Services[1].Method != "GET" {
t.Errorf("Method should have been %s (default)", "GET")
}
if config.Services[1].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
if !config.Services[1].ClientConfig.Insecure {
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[1].ClientConfig.Insecure)
}
if !config.Services[1].ClientConfig.IgnoreRedirect {
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Services[1].ClientConfig.IgnoreRedirect)
}
if config.Services[1].ClientConfig.Timeout != 5*time.Second {
t.Errorf("ClientConfig.Timeout should have been %v, got %v", 5*time.Second, config.Services[1].ClientConfig.Timeout)
}
if len(config.Services[1].Conditions) != 2 { if len(config.Services[1].Conditions) != 2 {
t.Errorf("There should have been %d conditions", 2) t.Errorf("There should have been %d conditions", 2)
} }
if config.Services[2].URL != "https://example.com/" {
t.Errorf("URL should have been %s", "https://example.com/")
}
if config.Services[2].Method != "GET" {
t.Errorf("Method should have been %s (default)", "GET")
}
if config.Services[2].Interval != 30*time.Minute {
t.Errorf("Interval should have been %s, because it is the default value", 30*time.Minute)
}
if !config.Services[2].ClientConfig.Insecure {
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[2].ClientConfig.Insecure)
}
if config.Services[2].ClientConfig.IgnoreRedirect {
t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", false, config.Services[2].ClientConfig.IgnoreRedirect)
}
if config.Services[2].ClientConfig.Timeout != 10*time.Second {
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", 10*time.Second, config.Services[2].ClientConfig.Timeout)
}
if len(config.Services[2].Conditions) != 1 {
t.Errorf("There should have been %d conditions", 1)
}
} }
func TestParseAndValidateConfigBytesDefault(t *testing.T) { func TestParseAndValidateConfigBytesDefault(t *testing.T) {
@ -104,17 +161,26 @@ services:
if config.Metrics { if config.Metrics {
t.Error("Metrics should've been false by default") t.Error("Metrics should've been false by default")
} }
if config.Web.Address != DefaultAddress {
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
}
if config.Web.Port != DefaultPort {
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
}
if config.Services[0].URL != "https://twinnation.org/health" { if config.Services[0].URL != "https://twinnation.org/health" {
t.Errorf("URL should have been %s", "https://twinnation.org/health") t.Errorf("URL should have been %s", "https://twinnation.org/health")
} }
if config.Services[0].Interval != 60*time.Second { if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if config.Web.Address != DefaultAddress { if config.Services[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress) t.Errorf("ClientConfig.Insecure should have been %v by default, got %v", true, config.Services[0].ClientConfig.Insecure)
} }
if config.Web.Port != DefaultPort { if config.Services[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
t.Errorf("Port should have been %d, because it is the default value", DefaultPort) t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", true, config.Services[0].ClientConfig.IgnoreRedirect)
}
if config.Services[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", client.GetDefaultConfig().Timeout, config.Services[0].ClientConfig.Timeout)
} }
} }
@ -143,11 +209,9 @@ services:
if config.Services[0].Interval != 60*time.Second { if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if config.Web.Address != "127.0.0.1" { if config.Web.Address != "127.0.0.1" {
t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1") t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1")
} }
if config.Web.Port != DefaultPort { if config.Web.Port != DefaultPort {
t.Errorf("Port should have been %d, because it is the default value", DefaultPort) t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
} }
@ -339,6 +403,8 @@ alerting:
integration-key: "00000000000000000000000000000000" integration-key: "00000000000000000000000000000000"
mattermost: mattermost:
webhook-url: "http://example.com" webhook-url: "http://example.com"
client:
insecure: true
messagebird: messagebird:
access-key: "1" access-key: "1"
originator: "31619191918" originator: "31619191918"
@ -895,6 +961,9 @@ services:
if config.Alerting.Custom.Insecure { if config.Alerting.Custom.Insecure {
t.Fatal("config.Alerting.Custom.Insecure shouldn't have been true") t.Fatal("config.Alerting.Custom.Insecure shouldn't have been true")
} }
if config.Alerting.Custom.ClientConfig.Insecure {
t.Errorf("ClientConfig.Insecure should have been %v, got %v", false, config.Alerting.Custom.ClientConfig.Insecure)
}
} }
func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndCustomPlaceholderValues(t *testing.T) { func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndCustomPlaceholderValues(t *testing.T) {

View File

@ -30,7 +30,6 @@ var (
Interval: 30 * time.Second, Interval: 30 * time.Second,
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition}, Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
Alerts: nil, Alerts: nil,
Insecure: false,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
} }

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -78,8 +79,13 @@ type Service struct {
Alerts []*alert.Alert `yaml:"alerts"` Alerts []*alert.Alert `yaml:"alerts"`
// Insecure is whether to skip verifying the server's certificate chain and host name // Insecure is whether to skip verifying the server's certificate chain and host name
//
// deprecated
Insecure bool `yaml:"insecure,omitempty"` Insecure bool `yaml:"insecure,omitempty"`
// ClientConfig is the configuration of the client used to communicate with the service's target
ClientConfig *client.Config `yaml:"client"`
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row // NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
NumberOfFailuresInARow int NumberOfFailuresInARow int
@ -90,6 +96,16 @@ type Service struct {
// ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one // ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
func (service *Service) ValidateAndSetDefaults() error { func (service *Service) ValidateAndSetDefaults() error {
// Set default values // Set default values
if service.ClientConfig == nil {
service.ClientConfig = client.GetDefaultConfig()
// XXX: remove the next 3 lines in v3.0.0
if service.Insecure {
log.Println("WARNING: services[].insecure has been deprecated and will be removed in v3.0.0 in favor of services[].client.insecure")
service.ClientConfig.Insecure = true
}
} else {
service.ClientConfig.ValidateAndSetDefaults()
}
if service.Interval == 0 { if service.Interval == 0 {
service.Interval = 1 * time.Minute service.Interval = 1 * time.Minute
} }
@ -199,7 +215,7 @@ func (service *Service) call(result *Result) {
service.DNS.query(service.URL, result) service.DNS.query(service.URL, result)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if isServiceStartTLS { } else if isServiceStartTLS {
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(service.URL, "starttls://"), service.Insecure) result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(service.URL, "starttls://"), service.ClientConfig)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())
return return
@ -207,12 +223,12 @@ func (service *Service) call(result *Result) {
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
result.CertificateExpiration = time.Until(certificate.NotAfter) result.CertificateExpiration = time.Until(certificate.NotAfter)
} else if isServiceTCP { } else if isServiceTCP {
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(service.URL, "tcp://")) result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(service.URL, "tcp://"), service.ClientConfig)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
} else if isServiceICMP { } else if isServiceICMP {
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(service.URL, "icmp://")) result.Connected, result.Duration = client.Ping(strings.TrimPrefix(service.URL, "icmp://"), service.ClientConfig)
} else { } else {
response, err = client.GetHTTPClient(service.Insecure).Do(request) response, err = client.GetHTTPClient(service.ClientConfig).Do(request)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
if err != nil { if err != nil {
result.AddError(err.Error()) result.AddError(err.Error())

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/TwinProduction/gatus/alerting/alert" "github.com/TwinProduction/gatus/alerting/alert"
"github.com/TwinProduction/gatus/client"
) )
func TestService_ValidateAndSetDefaults(t *testing.T) { func TestService_ValidateAndSetDefaults(t *testing.T) {
@ -18,6 +19,19 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}}, Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
} }
service.ValidateAndSetDefaults() service.ValidateAndSetDefaults()
if service.ClientConfig == nil {
t.Error("client configuration should've been set to the default configuration")
} else {
if service.ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
t.Errorf("Default client configuration should've set Insecure to %v, got %v", client.GetDefaultConfig().Insecure, service.ClientConfig.Insecure)
}
if service.ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
t.Errorf("Default client configuration should've set IgnoreRedirect to %v, got %v", client.GetDefaultConfig().IgnoreRedirect, service.ClientConfig.IgnoreRedirect)
}
if service.ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
t.Errorf("Default client configuration should've set Timeout to %v, got %v", client.GetDefaultConfig().Timeout, service.ClientConfig.Timeout)
}
}
if service.Method != "GET" { if service.Method != "GET" {
t.Error("Service method should've defaulted to GET") t.Error("Service method should've defaulted to GET")
} }
@ -41,6 +55,34 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
} }
} }
func TestService_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
condition := Condition("[STATUS] == 200")
service := Service{
Name: "twinnation-health",
URL: "https://twinnation.org/health",
Conditions: []*Condition{&condition},
ClientConfig: &client.Config{
Insecure: true,
IgnoreRedirect: true,
Timeout: 0,
},
}
service.ValidateAndSetDefaults()
if service.ClientConfig == nil {
t.Error("client configuration should've been set to the default configuration")
} else {
if !service.ClientConfig.Insecure {
t.Error("service.ClientConfig.Insecure should've been set to true")
}
if !service.ClientConfig.IgnoreRedirect {
t.Error("service.ClientConfig.IgnoreRedirect should've been set to true")
}
if service.ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
t.Error("service.ClientConfig.Timeout should've been set to 10s, because the timeout value entered is not set or invalid")
}
}
}
func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) { func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) {
defer func() { recover() }() defer func() { recover() }()
condition := Condition("[STATUS] == 200") condition := Condition("[STATUS] == 200")
@ -205,6 +247,7 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
URL: "https://twinnation.org/health", URL: "https://twinnation.org/health",
Conditions: []*Condition{&condition, &bodyCondition}, Conditions: []*Condition{&condition, &bodyCondition},
} }
service.ValidateAndSetDefaults()
result := service.EvaluateHealth() result := service.EvaluateHealth()
if !result.ConditionResults[0].Success { if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition) t.Errorf("Condition '%s' should have been a success", condition)
@ -224,6 +267,7 @@ func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
URL: "https://twinnation.org/health", URL: "https://twinnation.org/health",
Conditions: []*Condition{&condition}, Conditions: []*Condition{&condition},
} }
service.ValidateAndSetDefaults()
result := service.EvaluateHealth() result := service.EvaluateHealth()
if result.ConditionResults[0].Success { if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition) t.Errorf("Condition '%s' should have been a failure", condition)
@ -248,6 +292,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
}, },
Conditions: []*Condition{&conditionSuccess, &conditionBody}, Conditions: []*Condition{&conditionSuccess, &conditionBody},
} }
service.ValidateAndSetDefaults()
result := service.EvaluateHealth() result := service.EvaluateHealth()
if !result.ConditionResults[0].Success { if !result.ConditionResults[0].Success {
t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody) t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody)
@ -267,6 +312,7 @@ func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
URL: "icmp://127.0.0.1", URL: "icmp://127.0.0.1",
Conditions: []*Condition{&conditionSuccess}, Conditions: []*Condition{&conditionSuccess},
} }
service.ValidateAndSetDefaults()
result := service.EvaluateHealth() result := service.EvaluateHealth()
if !result.ConditionResults[0].Success { if !result.ConditionResults[0].Success {
t.Errorf("Conditions '%s' should have been a success", conditionSuccess) t.Errorf("Conditions '%s' should have been a success", conditionSuccess)