diff --git a/README.md b/README.md
index c7462504..1d82e7c7 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ For more details, see [Usage](#usage)
- [Conditions](#conditions)
- [Placeholders](#placeholders)
- [Functions](#functions)
+ - [Storage](#storage)
+ - [Client configuration](#client-configuration)
- [Alerting](#alerting)
- [Configuring Slack alerts](#configuring-slack-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[].url` | URL to send the request to. | Required `""` |
| `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[].interval` | Duration to wait between every status check. | `60s` |
| `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[].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[].client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` |
| `security` | 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.
+### 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
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
individual services with configurable descriptions and thresholds.
@@ -260,7 +298,7 @@ ignored.
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `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.access-key` | Messagebird access key | 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.url` | Custom alerting request url | Required `""` |
| `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.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.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 |
@@ -394,7 +432,8 @@ services:
alerting:
mattermost:
webhook-url: "http://**********/hooks/**********"
- insecure: true
+ client:
+ insecure: true
services:
- name: twinnation
@@ -490,7 +529,6 @@ alerting:
custom:
url: "https://hooks.slack.com/services/**********/**********/**********"
method: "POST"
- insecure: true
body: |
{
"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.
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**.
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 |
|:-------- |:------- |
| HTTP | 10s
-| TCP | 5s
+| TCP | 10s
+| ICMP | 10s
+
+To modify the timeout, see [Client configuration](#client-configuration).
### Monitoring a TCP service
-
By prefixing `services[].url` with `tcp:\\`, you can monitor TCP services at a very basic level:
```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).
[](https://github.com/math280h)
-[](https://github.com/mateothegreat)
diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go
index 344754f9..5398d10f 100644
--- a/alerting/provider/custom/custom.go
+++ b/alerting/provider/custom/custom.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/ioutil"
+ "log"
"net/http"
"os"
"strings"
@@ -19,18 +20,29 @@ import (
type AlertProvider struct {
URL string `yaml:"url"`
Method string `yaml:"method,omitempty"`
- Insecure bool `yaml:"insecure,omitempty"`
+ Insecure bool `yaml:"insecure,omitempty"` // deprecated
Body string `yaml:"body,omitempty"`
Headers map[string]string `yaml:"headers,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 *alert.Alert `yaml:"default-alert"`
}
// IsValid returns whether the provider's configuration is valid
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
@@ -103,7 +115,7 @@ func (provider *AlertProvider) Send(serviceName, alertDescription string, resolv
return []byte("{}"), nil
}
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 {
return nil, err
}
diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go
index 4b0f4271..5538ba3f 100644
--- a/alerting/provider/mattermost/mattermost.go
+++ b/alerting/provider/mattermost/mattermost.go
@@ -2,17 +2,22 @@ package mattermost
import (
"fmt"
+ "log"
"net/http"
"github.com/TwinProduction/gatus/alerting/alert"
"github.com/TwinProduction/gatus/alerting/provider/custom"
+ "github.com/TwinProduction/gatus/client"
"github.com/TwinProduction/gatus/core"
)
// AlertProvider is the configuration necessary for sending an alert using Mattermost
type AlertProvider struct {
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 *alert.Alert `yaml:"default-alert"`
@@ -20,6 +25,14 @@ type AlertProvider struct {
// IsValid returns whether the provider's configuration is valid
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
}
@@ -45,9 +58,9 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
}
return &custom.AlertProvider{
- URL: provider.WebhookURL,
- Method: http.MethodPost,
- Insecure: provider.Insecure,
+ URL: provider.WebhookURL,
+ Method: http.MethodPost,
+ ClientConfig: provider.ClientConfig,
Body: fmt.Sprintf(`{
"text": "",
"username": "gatus",
diff --git a/client/client.go b/client/client.go
index 314f5e94..fc2fce02 100644
--- a/client/client.go
+++ b/client/client.go
@@ -14,58 +14,14 @@ import (
"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
-func GetHTTPClient(insecure bool) *http.Client {
- if insecure {
- 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
+func GetHTTPClient(config *Config) *http.Client {
+ return config.GetHTTPClient()
}
// CanCreateTCPConnection checks whether a connection can be established with a TCP service
-func CanCreateTCPConnection(address string) bool {
- conn, err := net.DialTimeout("tcp", address, 5*time.Second)
+func CanCreateTCPConnection(address string, config *Config) bool {
+ conn, err := net.DialTimeout("tcp", address, config.Timeout)
if err != nil {
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
-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, ":")
if len(hostAndPort) != 2 {
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
}
err = smtpClient.StartTLS(&tls.Config{
- InsecureSkipVerify: insecure,
+ InsecureSkipVerify: config.Insecure,
ServerName: hostAndPort[0],
})
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
//
// 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)
if err != nil {
return false, 0
}
pinger.Count = 1
- pinger.Timeout = pingTimeout
+ pinger.Timeout = config.Timeout
// Set the pinger's privileged mode to true for every operating system except darwin
// https://github.com/TwinProduction/gatus/issues/132
pinger.SetPrivileged(runtime.GOOS != "darwin")
diff --git a/client/client_test.go b/client/client_test.go
index 4dc15202..43f1d062 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -6,43 +6,28 @@ import (
)
func TestGetHTTPClient(t *testing.T) {
- if secureHTTPClient != nil {
- t.Error("secureHTTPClient should've been nil since it hasn't been called a single time yet")
- }
- if insecureHTTPClient != nil {
- t.Error("insecureHTTPClient should've been nil since it hasn't been called a single time yet")
- }
- _ = 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")
- }
+ GetHTTPClient(&Config{
+ Insecure: false,
+ IgnoreRedirect: false,
+ Timeout: 0,
+ httpClient: nil,
+ })
}
func TestPing(t *testing.T) {
- pingTimeout = 500 * time.Millisecond
- if success, rtt := Ping("127.0.0.1"); !success {
+ if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond}); !success {
t.Error("expected true")
if rtt == 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")
if rtt != 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")
if rtt != 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 {
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 {
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
return
@@ -101,7 +86,7 @@ func TestCanPerformStartTLS(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")
}
}
diff --git a/client/config.go b/client/config.go
new file mode 100644
index 00000000..e8c45006
--- /dev/null
+++ b/client/config.go
@@ -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
+}
diff --git a/client/config_test.go b/client/config_test.go
new file mode 100644
index 00000000..7fe6576f
--- /dev/null
+++ b/client/config_test.go
@@ -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")
+ }
+}
diff --git a/config/config_test.go b/config/config_test.go
index c9cb1d31..6d0442a4 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -16,6 +16,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/slack"
"github.com/TwinProduction/gatus/alerting/provider/telegram"
"github.com/TwinProduction/gatus/alerting/provider/twilio"
+ "github.com/TwinProduction/gatus/client"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/k8stest"
v1 "k8s.io/api/core/v1"
@@ -40,17 +41,31 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
storage:
file: %s
+
services:
- name: twinnation
url: https://twinnation.org/health
interval: 15s
conditions:
- "[STATUS] == 200"
+
- name: github
url: https://api.github.com/healthz
+ client:
+ insecure: true
+ ignore-redirect: true
+ timeout: 5s
conditions:
- "[STATUS] != 400"
- "[STATUS] != 500"
+
+ - name: example
+ url: https://example.com/
+ interval: 30m
+ client:
+ insecure: true
+ conditions:
+ - "[STATUS] == 200"
`, file)))
if err != nil {
t.Error("expected no error, got", err.Error())
@@ -58,33 +73,75 @@ services:
if config == 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")
}
+
if config.Services[0].URL != "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" {
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 {
t.Errorf("Interval should have been %s", 15*time.Second)
}
- 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[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
+ 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 {
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 {
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) {
@@ -104,17 +161,26 @@ services:
if config.Metrics {
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" {
t.Errorf("URL should have been %s", "https://twinnation.org/health")
}
if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
- if config.Web.Address != DefaultAddress {
- t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
+ if config.Services[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
+ t.Errorf("ClientConfig.Insecure should have been %v by default, got %v", true, config.Services[0].ClientConfig.Insecure)
}
- if config.Web.Port != DefaultPort {
- t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
+ if config.Services[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
+ 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 {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
}
-
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")
}
-
if config.Web.Port != DefaultPort {
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
}
@@ -339,6 +403,8 @@ alerting:
integration-key: "00000000000000000000000000000000"
mattermost:
webhook-url: "http://example.com"
+ client:
+ insecure: true
messagebird:
access-key: "1"
originator: "31619191918"
@@ -895,6 +961,9 @@ services:
if config.Alerting.Custom.Insecure {
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) {
diff --git a/controller/controller_test.go b/controller/controller_test.go
index 269ca5fe..b12c4656 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -30,7 +30,6 @@ var (
Interval: 30 * time.Second,
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
Alerts: nil,
- Insecure: false,
NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0,
}
diff --git a/core/service.go b/core/service.go
index 9ff23311..c62ef9a3 100644
--- a/core/service.go
+++ b/core/service.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"io/ioutil"
+ "log"
"net"
"net/http"
"net/url"
@@ -78,8 +79,13 @@ type Service struct {
Alerts []*alert.Alert `yaml:"alerts"`
// Insecure is whether to skip verifying the server's certificate chain and host name
+ //
+ // deprecated
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 int
@@ -90,6 +96,16 @@ type Service struct {
// ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
func (service *Service) ValidateAndSetDefaults() error {
// 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 {
service.Interval = 1 * time.Minute
}
@@ -199,7 +215,7 @@ func (service *Service) call(result *Result) {
service.DNS.query(service.URL, result)
result.Duration = time.Since(startTime)
} 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 {
result.AddError(err.Error())
return
@@ -207,12 +223,12 @@ func (service *Service) call(result *Result) {
result.Duration = time.Since(startTime)
result.CertificateExpiration = time.Until(certificate.NotAfter)
} 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)
} 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 {
- response, err = client.GetHTTPClient(service.Insecure).Do(request)
+ response, err = client.GetHTTPClient(service.ClientConfig).Do(request)
result.Duration = time.Since(startTime)
if err != nil {
result.AddError(err.Error())
diff --git a/core/service_test.go b/core/service_test.go
index 712c6bae..ab6d0ce1 100644
--- a/core/service_test.go
+++ b/core/service_test.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/TwinProduction/gatus/alerting/alert"
+ "github.com/TwinProduction/gatus/client"
)
func TestService_ValidateAndSetDefaults(t *testing.T) {
@@ -18,6 +19,19 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
}
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" {
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) {
defer func() { recover() }()
condition := Condition("[STATUS] == 200")
@@ -205,6 +247,7 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
URL: "https://twinnation.org/health",
Conditions: []*Condition{&condition, &bodyCondition},
}
+ service.ValidateAndSetDefaults()
result := service.EvaluateHealth()
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
@@ -224,6 +267,7 @@ func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
URL: "https://twinnation.org/health",
Conditions: []*Condition{&condition},
}
+ service.ValidateAndSetDefaults()
result := service.EvaluateHealth()
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
@@ -248,6 +292,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
},
Conditions: []*Condition{&conditionSuccess, &conditionBody},
}
+ service.ValidateAndSetDefaults()
result := service.EvaluateHealth()
if !result.ConditionResults[0].Success {
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",
Conditions: []*Condition{&conditionSuccess},
}
+ service.ValidateAndSetDefaults()
result := service.EvaluateHealth()
if !result.ConditionResults[0].Success {
t.Errorf("Conditions '%s' should have been a success", conditionSuccess)