#126: Add client configuration
This commit is contained in:
		
							
								
								
									
										57
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								README.md
									
									
									
									
									
								
							| @ -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) |  | ||||||
|  | |||||||
| @ -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 | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -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") | ||||||
|  | |||||||
| @ -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
									
								
							
							
						
						
									
										73
									
								
								client/config.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										37
									
								
								client/config_test.go
									
									
									
									
									
										Normal 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") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -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) { | ||||||
|  | |||||||
| @ -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, | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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()) | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user