From 9cd63550561b6a500ca7512df0031818d5714b23 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Wed, 28 Jul 2021 21:41:26 -0400 Subject: [PATCH 1/3] #126: Add client configuration --- README.md | 57 +++++++++++-- alerting/provider/custom/custom.go | 18 +++- alerting/provider/mattermost/mattermost.go | 21 ++++- client/client.go | 60 ++----------- client/client_test.go | 37 +++----- client/config.go | 73 ++++++++++++++++ client/config_test.go | 37 ++++++++ config/config_test.go | 99 ++++++++++++++++++---- controller/controller_test.go | 1 - core/service.go | 24 +++++- core/service_test.go | 46 ++++++++++ 11 files changed, 359 insertions(+), 114 deletions(-) create mode 100644 client/config.go create mode 100644 client/config_test.go 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) From d3e0ef651918145a03882ae76bbc3b16a44718d8 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Wed, 28 Jul 2021 22:25:25 -0400 Subject: [PATCH 2/3] Add workflow to publish latest on successful build --- .github/workflows/publish-latest.yml | 35 +++++++++++++++++++ .../{publish.yml => publish-release.yml} | 6 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-latest.yml rename .github/workflows/{publish.yml => publish-release.yml} (93%) diff --git a/.github/workflows/publish-latest.yml b/.github/workflows/publish-latest.yml new file mode 100644 index 00000000..2f003246 --- /dev/null +++ b/.github/workflows/publish-latest.yml @@ -0,0 +1,35 @@ +name: publish-latest +on: + workflow_run: + workflows: ["Build"] + types: [completed] +jobs: + publish-latest: + name: Publish latest + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + timeout-minutes: 30 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Get image repository + run: echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV + - name: Get the release + run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build and push docker image + uses: docker/build-push-action@v2 + with: + platforms: linux/amd64 + pull: true + push: true + tags: | + ${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish-release.yml similarity index 93% rename from .github/workflows/publish.yml rename to .github/workflows/publish-release.yml index f5bec528..f43dc808 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish-release.yml @@ -1,10 +1,10 @@ -name: publish +name: publish-release on: release: types: [published] jobs: - build: - name: Publish + publish-release: + name: Publish release runs-on: ubuntu-latest timeout-minutes: 30 steps: From 07b1a2eafb33ca6db0e75c7894b3ec54999debbc Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Wed, 28 Jul 2021 22:30:34 -0400 Subject: [PATCH 3/3] Minor fix --- .github/workflows/publish-latest.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-latest.yml b/.github/workflows/publish-latest.yml index 2f003246..15b836b8 100644 --- a/.github/workflows/publish-latest.yml +++ b/.github/workflows/publish-latest.yml @@ -1,7 +1,7 @@ name: publish-latest on: workflow_run: - workflows: ["Build"] + workflows: ["build"] types: [completed] jobs: publish-latest: @@ -14,8 +14,6 @@ jobs: uses: actions/checkout@v2 - name: Get image repository run: echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - - name: Get the release - run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx @@ -32,4 +30,4 @@ jobs: pull: true push: true tags: | - ${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }} + ${{ env.IMAGE_REPOSITORY }}:latest