Compare commits

..

12 Commits

Author SHA1 Message Date
TwiN
447e140479 feat(connectivity): Allow internet connection validation prior to endpoint execution (#461) 2023-05-02 22:41:22 -04:00
dependabot[bot]
6908199716 chore(deps): bump golang.org/x/crypto from 0.7.0 to 0.8.0 (#455)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-30 23:35:14 -04:00
dependabot[bot]
b12f652553 chore(deps): bump codecov/codecov-action from 3.1.2 to 3.1.3 (#459)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-26 22:39:19 -04:00
TwiN
83edca6e80 refactor: Modify implementation of TLS (#457)
* refactor: Don't generate certificates programmatically

* build: Add testdata folder to .dockerignore
2023-04-22 15:22:09 -04:00
TwiN
636688b43e chore(deps): Upgrade github.com/TwiN/g8 to v2.0.0 2023-04-22 12:55:30 -04:00
TwiN
4fdb55d632 chore: Update Go to 1.20 2023-04-22 12:13:37 -04:00
Christian Krudewig
a05daeda2e feat(web): Support TLS encryption (#322)
* Basic setup to serve HTTPS

* Correctly handle the case of missing TLS configs

* Documenting TLS

* Refactor TLS configuration setup

* Add TLS Encryption section again to README

* Extending TOC in README

* Moving TLS settings to subsection of web settings

* Adding tests for config/web

* Add test for handling TLS

* Rename some variables as suggested

* Corrected error formatting

* Update test module import

* Polishing the readme file

* Error handling for TLSConfig()

---------

Co-authored-by: TwiN <twin@linux.com>
2023-04-22 12:12:56 -04:00
TwiN
0bd0c1fd15 docs(sponsors): Add @8ball030 to list of sponsors 2023-04-20 18:28:03 -04:00
dependabot[bot]
eb3ca71c72 chore(deps): bump golang.org/x/oauth2 from 0.6.0 to 0.7.0 (#454)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-17 20:01:08 -04:00
dependabot[bot]
37325cd78a chore(deps): bump codecov/codecov-action from 3.1.1 to 3.1.2 (#453)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 12:01:44 -04:00
dependabot[bot]
f6e7e346b6 chore(deps): bump golang.org/x/oauth2 from 0.5.0 to 0.6.0 (#450)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-06 20:14:16 -04:00
TwiN
b5e742acde fix(ui): Hide endpoint and prev/next arrows while data is being fetched 2023-04-05 19:33:50 -04:00
23 changed files with 404 additions and 61 deletions

View File

@@ -4,4 +4,5 @@ Dockerfile
.idea
.git
web/app
*.db
*.db
testdata

View File

@@ -28,6 +28,6 @@ jobs:
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
- name: Codecov
uses: codecov/codecov-action@v3.1.1
uses: codecov/codecov-action@v3.1.3
with:
files: ./coverage.txt

View File

@@ -67,9 +67,11 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Setting a default alert](#setting-a-default-alert)
- [Maintenance](#maintenance)
- [Security](#security)
- [Basic](#basic)
- [Basic Authentication](#basic-authentication)
- [OIDC](#oidc)
- [TLS Encryption](#tls-encryption)
- [Metrics](#metrics)
- [Connectivity](#connectivity)
- [Remote instances (EXPERIMENTAL)](#remote-instances-experimental)
- [Deployment](#deployment)
- [Docker](#docker)
@@ -87,7 +89,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)>
- [Monitoring domain expiration](#monitoring-domain-expiration)
- [disable-monitoring-lock](#disable-monitoring-lock)
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
@@ -228,6 +230,8 @@ If you want to test it locally, see [Docker](#docker).
| `web` | Web configuration. | `{}` |
| `web.address` | Address to listen on. | `0.0.0.0` |
| `web.port` | Port to listen on. | `8080` |
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
| `ui` | UI configuration. | `{}` |
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
@@ -1053,13 +1057,13 @@ As a result, the `[ALERT_TRIGGERED_OR_RESOLVED]` in the body of first example of
#### Setting a default alert
| Parameter | Description | Default |
|:----------------------------------------------|:------------------------------------------------------------------------------|:--------|
| `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 |
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
| Parameter | Description | Default |
|:---------------------------------------------|:------------------------------------------------------------------------------|:--------|
| `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 |
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
> ⚠ You must still specify the `type` of the alert in the endpoint configuration even if you set the default alert of a provider.
@@ -1175,14 +1179,14 @@ maintenance:
### Security
| Parameter | Description | Default |
|:---------------------------------|:-----------------------------|:--------------|
| `security` | Security configuration | `{}` |
| `security.basic` | HTTP Basic configuration | `{}` |
| `security.oidc` | OpenID Connect configuration | `{}` |
| Parameter | Description | Default |
|:-----------------|:-----------------------------|:--------|
| `security` | Security configuration | `{}` |
| `security.basic` | HTTP Basic configuration | `{}` |
| `security.oidc` | OpenID Connect configuration | `{}` |
#### Basic
#### Basic Authentication
| Parameter | Description | Default |
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
| `security.basic` | HTTP Basic configuration | `{}` |
@@ -1226,6 +1230,17 @@ security:
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
### TLS Encryption
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
The example below shows an example configuration which makes gatus respond on port 4443 to HTTPS requests.
```yaml
web:
port: 4443
tls:
certificate-file: "server.crt"
private-key-file: "server.key"
```
### Metrics
To enable metrics, you must set `metrics` to `true`. Doing so will expose Prometheus-friendly metrics at the `/metrics`
@@ -1242,6 +1257,28 @@ endpoint on the same port your application is configured to run on (`web.port`).
See [examples/docker-compose-grafana-prometheus](.examples/docker-compose-grafana-prometheus) for further documentation as well as an example.
### Connectivity
| Parameter | Description | Default |
|:--------------------------------|:-------------------------------------------|:--------------|
| `connectivity` | Connectivity configuration | `{}` |
| `connectivity.checker` | Connectivity checker configuration | Required `{}` |
| `connectivity.checker.target` | Host to use for validating connectivity | Required `""` |
| `connectivity.checker.interval` | Interval at which to validate connectivity | `1m` |
While Gatus is used to monitor other services, it is possible for Gatus itself to lose connectivity to the internet.
In order to prevent Gatus from reporting endpoints as unhealthy when Gatus itself is unhealthy, you may configure
Gatus to periodically check for internet connectivity.
All endpoint executions are skipped while the connectivity checker deems connectivity to be down.
```yaml
connectivity:
checker:
target: 1.1.1.1:53
interval: 60s
```
### Remote instances (EXPERIMENTAL)
This feature allows you to retrieve endpoint statuses from a remote Gatus instance.
@@ -1253,12 +1290,12 @@ This is an experimental feature. It may be removed or updated in a breaking mann
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
Use at your own risk.
| Parameter | Description | Default |
|:-----------------------------------|:---------------------------------------------|:---------------|
| `remote` | Remote configuration | `{}` |
| `remote.instances` | List of remote instances | Required `[]` |
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
| Parameter | Description | Default |
|:-----------------------------------|:---------------------------------------------|:--------------|
| `remote` | Remote configuration | `{}` |
| `remote.instances` | List of remote instances | Required `[]` |
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
```yaml
remote:
@@ -1386,11 +1423,11 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
### Default timeouts
| Endpoint type | Timeout |
|:---------------|:--------|
| HTTP | 10s |
| TCP | 10s |
| ICMP | 10s |
| Endpoint type | Timeout |
|:--------------|:--------|
| HTTP | 10s |
| TCP | 10s |
| ICMP | 10s |
To modify the timeout, see [Client configuration](#client-configuration).
@@ -1784,6 +1821,8 @@ No such header is required to query the API.
## Sponsors
You can find the full list of sponsors [here](https://github.com/sponsors/TwiN).
_There is currently no sponsors_
<!-- _There is currently no sponsors_ -->
[<img src="https://github.com/8ball030.png" width="50" />](https://github.com/8ball030)
<!-- [<img src="https://github.com/$user.png" width="50" />](https://github.com/$user) -->

View File

@@ -191,6 +191,9 @@ func TestCanCreateTCPConnection(t *testing.T) {
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
t.Error("should've failed, because there's no port in the address")
}
if !CanCreateTCPConnection("1.1.1.1:53", &Config{Timeout: 5 * time.Second}) {
t.Error("should've succeeded, because that IP should always™ be up")
}
}
// This test checks if a HTTP client configured with `configureOAuth2()` automatically

View File

@@ -14,6 +14,7 @@ import (
"github.com/TwiN/gatus/v5/alerting"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider"
"github.com/TwiN/gatus/v5/config/connectivity"
"github.com/TwiN/gatus/v5/config/maintenance"
"github.com/TwiN/gatus/v5/config/remote"
"github.com/TwiN/gatus/v5/config/ui"
@@ -91,6 +92,9 @@ type Config struct {
// WARNING: This is in ALPHA and may change or be completely removed in the future
Remote *remote.Config `yaml:"remote,omitempty"`
// Connectivity is the configuration for connectivity
Connectivity *connectivity.Config `yaml:"connectivity,omitempty"`
configPath string // path to the file or directory from which config was loaded
lastFileModTime time.Time // last modification time
}
@@ -252,10 +256,20 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
if err := validateRemoteConfig(config); err != nil {
return nil, err
}
if err := validateConnectivityConfig(config); err != nil {
return nil, err
}
}
return
}
func validateConnectivityConfig(config *Config) error {
if config.Connectivity != nil {
return config.Connectivity.ValidateAndSetDefaults()
}
return nil
}
func validateRemoteConfig(config *Config) error {
if config.Remote != nil {
if err := config.Remote.ValidateAndSetDefaults(); err != nil {

View File

@@ -0,0 +1,53 @@
package connectivity
import (
"errors"
"strings"
"time"
"github.com/TwiN/gatus/v5/client"
)
var (
ErrInvalidInterval = errors.New("connectivity.checker.interval must be 5s or higher")
ErrInvalidDNSTarget = errors.New("connectivity.checker.target must be suffixed with :53")
)
// Config is the configuration for the connectivity checker.
type Config struct {
Checker *Checker `yaml:"checker,omitempty"`
}
func (c *Config) ValidateAndSetDefaults() error {
if c.Checker != nil {
if c.Checker.Interval == 0 {
c.Checker.Interval = 60 * time.Second
} else if c.Checker.Interval < 5*time.Second {
return ErrInvalidInterval
}
if !strings.HasSuffix(c.Checker.Target, ":53") {
return ErrInvalidDNSTarget
}
}
return nil
}
// Checker is the configuration for making sure Gatus has access to the internet.
type Checker struct {
Target string `yaml:"target"` // e.g. 1.1.1.1:53
Interval time.Duration `yaml:"interval,omitempty"`
isConnected bool
lastCheck time.Time
}
func (c Checker) Check() bool {
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
}
func (c *Checker) IsConnected() bool {
if now := time.Now(); now.After(c.lastCheck.Add(c.Interval)) {
c.lastCheck, c.isConnected = now, c.Check()
}
return c.isConnected
}

View File

@@ -0,0 +1,62 @@
package connectivity
import (
"fmt"
"testing"
"time"
)
func TestConfig(t *testing.T) {
scenarios := []struct {
name string
cfg *Config
expectedErr error
expectedInterval time.Duration
}{
{
name: "good-config",
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}},
expectedInterval: 10 * time.Second,
},
{
name: "good-config-with-default-interval",
cfg: &Config{Checker: &Checker{Target: "8.8.8.8:53", Interval: 0}},
expectedInterval: 60 * time.Second,
},
{
name: "config-with-interval-too-low",
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 4 * time.Second}},
expectedErr: ErrInvalidInterval,
},
{
name: "config-with-invalid-target-due-to-missing-port",
cfg: &Config{Checker: &Checker{Target: "1.1.1.1", Interval: 15 * time.Second}},
expectedErr: ErrInvalidDNSTarget,
},
{
name: "config-with-invalid-target-due-to-invalid-dns-port",
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:52", Interval: 15 * time.Second}},
expectedErr: ErrInvalidDNSTarget,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
err := scenario.cfg.ValidateAndSetDefaults()
if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", scenario.expectedErr) {
t.Errorf("expected error %v, got %v", scenario.expectedErr, err)
}
if err == nil && scenario.expectedErr == nil {
if scenario.cfg.Checker.Interval != scenario.expectedInterval {
t.Errorf("expected interval %v, got %v", scenario.expectedInterval, scenario.cfg.Checker.Interval)
}
}
})
}
}
func TestChecker_IsConnected(t *testing.T) {
checker := &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}
if !checker.IsConnected() {
t.Error("expected checker.IsConnected() to be true")
}
}

View File

@@ -1,6 +1,8 @@
package web
import (
"crypto/tls"
"errors"
"fmt"
"math"
)
@@ -21,6 +23,19 @@ type Config struct {
// Port to listen on (default to 8080 specified by DefaultPort)
Port int `yaml:"port"`
// TLS configuration (optional)
TLS *TLSConfig `yaml:"tls,omitempty"`
}
type TLSConfig struct {
// CertificateFile is the public certificate for TLS in PEM format.
CertificateFile string `yaml:"certificate-file,omitempty"`
// PrivateKeyFile is the private key file for TLS in PEM format.
PrivateKeyFile string `yaml:"private-key-file,omitempty"`
tlsConfig *tls.Config
}
// GetDefaultConfig returns a Config struct with the default values
@@ -40,6 +55,12 @@ func (web *Config) ValidateAndSetDefaults() error {
} else if web.Port < 0 || web.Port > math.MaxUint16 {
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
}
// Try to load the TLS certificates
if web.TLS != nil {
if err := web.TLS.loadConfig(); err != nil {
return fmt.Errorf("invalid tls config: %w", err)
}
}
return nil
}
@@ -47,3 +68,22 @@ func (web *Config) ValidateAndSetDefaults() error {
func (web *Config) SocketAddress() string {
return fmt.Sprintf("%s:%d", web.Address, web.Port)
}
func (t *TLSConfig) loadConfig() error {
if len(t.CertificateFile) > 0 && len(t.PrivateKeyFile) > 0 {
certificate, err := tls.LoadX509KeyPair(t.CertificateFile, t.PrivateKeyFile)
if err != nil {
return err
}
t.tlsConfig = &tls.Config{Certificates: []tls.Certificate{certificate}}
return nil
}
return errors.New("certificate-file and private-key-file must be specified")
}
func (web *Config) TLSConfig() *tls.Config {
if web.TLS != nil {
return web.TLS.tlsConfig
}
return nil
}

View File

@@ -12,6 +12,9 @@ func TestGetDefaultConfig(t *testing.T) {
if defaultConfig.Address != DefaultAddress {
t.Error("expected default config to have the default address")
}
if defaultConfig.TLS != nil {
t.Error("expected default config to have TLS disabled")
}
}
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
@@ -63,3 +66,56 @@ func TestConfig_SocketAddress(t *testing.T) {
t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress())
}
}
func TestConfig_TLSConfig(t *testing.T) {
scenarios := []struct {
name string
cfg *Config
expectedErr bool
}{
{
name: "good-tls-config",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
expectedErr: false,
},
{
name: "missing-crt-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "doesnotexist", PrivateKeyFile: "../../testdata/cert.key"}},
expectedErr: true,
},
{
name: "bad-crt-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
expectedErr: true,
},
{
name: "missing-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "doesnotexist"}},
expectedErr: true,
},
{
name: "bad-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
expectedErr: true,
},
{
name: "bad-cert-and-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
expectedErr: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
err := scenario.cfg.ValidateAndSetDefaults()
if (err != nil) != scenario.expectedErr {
t.Errorf("expected the existence of an error to be %v, got %v", scenario.expectedErr, err)
return
}
if !scenario.expectedErr {
if scenario.cfg.TLS.tlsConfig == nil {
t.Error("TLS configuration was not correctly loaded although no error was returned")
}
}
})
}
}

View File

@@ -24,8 +24,10 @@ func Handle(cfg *config.Config) {
if os.Getenv("ENVIRONMENT") == "dev" {
router = handler.DevelopmentCORS(router)
}
tlsConfig := cfg.Web.TLSConfig()
server = &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
TLSConfig: tlsConfig,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
@@ -35,7 +37,11 @@ func Handle(cfg *config.Config) {
if os.Getenv("ROUTER_TEST") == "true" {
return
}
log.Println("[controller][Handle]", server.ListenAndServe())
if tlsConfig != nil {
log.Println("[controller][Handle]", server.ListenAndServeTLS("", ""))
} else {
log.Println("[controller][Handle]", server.ListenAndServe())
}
}
// Shutdown stops the server

View File

@@ -45,6 +45,48 @@ func TestHandle(t *testing.T) {
}
}
func TestHandleTLS(t *testing.T) {
scenarios := []struct {
name string
tls *web.TLSConfig
expectedStatusCode int
}{
{
name: "good-tls-config",
tls: &web.TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedStatusCode: 200,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
cfg := &config.Config{
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
Endpoints: []*core.Endpoint{
{Name: "frontend", Group: "core"},
{Name: "backend", Group: "core"},
},
}
if err := cfg.Web.ValidateAndSetDefaults(); err != nil {
t.Error("expected no error from web (TLS) validation, got", err)
}
_ = os.Setenv("ROUTER_TEST", "true")
_ = os.Setenv("ENVIRONMENT", "dev")
defer os.Clearenv()
Handle(cfg)
defer Shutdown()
request, _ := http.NewRequest("GET", "/health", http.NoBody)
responseRecorder := httptest.NewRecorder()
server.Handler.ServeHTTP(responseRecorder, request)
if responseRecorder.Code != scenario.expectedStatusCode {
t.Errorf("expected GET /health to return status code %d, got %d", scenario.expectedStatusCode, responseRecorder.Code)
}
if server == nil {
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
}
})
}
}
func TestShutdown(t *testing.T) {
// Pretend that we called controller.Handle(), which initializes the server variable
server = &http.Server{}

12
go.mod
View File

@@ -1,10 +1,10 @@
module github.com/TwiN/gatus/v5
go 1.19
go 1.20
require (
github.com/TwiN/deepmerge v0.2.0
github.com/TwiN/g8 v1.4.0
github.com/TwiN/g8/v2 v2.0.0
github.com/TwiN/gocache/v2 v2.2.0
github.com/TwiN/health v1.6.0
github.com/TwiN/whois v1.1.0
@@ -18,8 +18,8 @@ require (
github.com/prometheus-community/pro-bing v0.1.0
github.com/prometheus/client_golang v1.14.0
github.com/wcharczuk/go-chart/v2 v2.1.0
golang.org/x/crypto v0.7.0
golang.org/x/oauth2 v0.5.0
golang.org/x/crypto v0.8.0
golang.org/x/oauth2 v0.7.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.20.4
@@ -43,9 +43,9 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/tools v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect

20
go.sum
View File

@@ -36,8 +36,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/TwiN/deepmerge v0.2.0 h1:8P1tp2qDXllX6V1Ailg2XA074easAcvLMmW9v1jn0aE=
github.com/TwiN/deepmerge v0.2.0/go.mod h1:cR9OWsvI13y+FxrbnPLuF6BX2tbYeOjkiI6JWjEGqAE=
github.com/TwiN/g8 v1.4.0 h1:RUk5xTtxKCdMo0GGSbBVyjtAAfi2nqVbA9E0C4u5Cxo=
github.com/TwiN/g8 v1.4.0/go.mod h1:ECyGJsoIb99klUfvVQoS1StgRLte9yvvPigGrHdy284=
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
github.com/TwiN/g8/v2 v2.0.0/go.mod h1:4sVAF27q8T8ISggRa/Fb0drw7wpB22B6eWd+/+SGMqE=
github.com/TwiN/gocache/v2 v2.2.0 h1:M3B36KyH24BntxLrLaUb2kgTdq8DzCnfod0IekLG57w=
github.com/TwiN/gocache/v2 v2.2.0/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
github.com/TwiN/health v1.6.0 h1:L2ks575JhRgQqWWOfKjw9B0ec172hx7GdToqkYUycQM=
@@ -261,8 +261,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -334,8 +334,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -344,8 +344,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -400,8 +400,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=

View File

@@ -4,7 +4,7 @@ import (
"encoding/base64"
"net/http"
"github.com/TwiN/g8"
g8 "github.com/TwiN/g8/v2"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)

3
testdata/badcert.key vendored Normal file
View File

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
wat
-----END PRIVATE KEY-----

3
testdata/badcert.pem vendored Normal file
View File

@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
wat
-----END CERTIFICATE-----

5
testdata/cert.key vendored Normal file
View File

@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJh67FWpz8wrN1mM/
CebkZN0zF83691ZVD83XlbNLRUqhRANCAAScfyPxScqz+Z/yNtAID/FOORy9J6LM
DUAJevGDvAZCMp/nh+Ps3nLrMoRlykcux3mq+N8HPlJ8R3eetB4S1tHY
-----END PRIVATE KEY-----

10
testdata/cert.pem vendored Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBaDCCAQ2gAwIBAgICBNIwCgYIKoZIzj0EAwIwFTETMBEGA1UEChMKR2F0dXMg
dGVzdDAgFw0yMzA0MjIxODUwMDVaGA8yMjk3MDIwNDE4NTAwNVowFTETMBEGA1UE
ChMKR2F0dXMgdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJx/I/FJyrP5
n/I20AgP8U45HL0noswNQAl68YO8BkIyn+eH4+zecusyhGXKRy7Hear43wc+UnxH
d560HhLW0dijSzBJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
ATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggqhkjOPQQD
AgNJADBGAiEA/SdthKOoNw3azSHuPid7XJsXYB8DisIC9LBwcb/QTMECIQCAB36Y
OI15ao+J/RUz2sXdPXCAN8hlohi6OnmZmJB32g==
-----END CERTIFICATE-----

View File

@@ -8,6 +8,7 @@ import (
"github.com/TwiN/gatus/v5/alerting"
"github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/connectivity"
"github.com/TwiN/gatus/v5/config/maintenance"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/metrics"
@@ -30,15 +31,15 @@ func Monitor(cfg *config.Config) {
if endpoint.IsEnabled() {
// To prevent multiple requests from running at the same time, we'll wait for a little before each iteration
time.Sleep(777 * time.Millisecond)
go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.Connectivity, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
}
}
}
// monitor a single endpoint in a loop
func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
// Run it immediately on start
execute(endpoint, alertingConfig, maintenanceConfig, disableMonitoringLock, enabledMetrics, debug)
execute(endpoint, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, debug)
// Loop for the next executions
for {
select {
@@ -46,16 +47,22 @@ func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
log.Printf("[watchdog][monitor] Canceling current execution of group=%s; endpoint=%s", endpoint.Group, endpoint.Name)
return
case <-time.After(endpoint.Interval):
execute(endpoint, alertingConfig, maintenanceConfig, disableMonitoringLock, enabledMetrics, debug)
execute(endpoint, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, debug)
}
}
}
func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, disableMonitoringLock, enabledMetrics, debug bool) {
func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock, enabledMetrics, debug bool) {
if !disableMonitoringLock {
// By placing the lock here, we prevent multiple endpoints from being monitored at the exact same time, which
// could cause performance issues and return inaccurate results
monitoringMutex.Lock()
defer monitoringMutex.Unlock()
}
// If there's a connectivity checker configured, check if Gatus has internet connectivity
if connectivityConfig != nil && connectivityConfig.Checker != nil && !connectivityConfig.Checker.IsConnected() {
log.Println("[watchdog][execute] No connectivity; skipping execution")
return
}
if debug {
log.Printf("[watchdog][execute] Monitoring group=%s; endpoint=%s", endpoint.Group, endpoint.Name)
@@ -79,9 +86,6 @@ func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
if debug {
log.Printf("[watchdog][execute] Waiting for interval=%s before monitoring group=%s endpoint=%s again", endpoint.Interval, endpoint.Group, endpoint.Name)
}
if !disableMonitoringLock {
monitoringMutex.Unlock()
}
}
// UpdateEndpointStatuses updates the slice of endpoint statuses

View File

@@ -1,14 +1,15 @@
<template>
<Loading v-if="!retrievedData" class="h-64 w-64 px-4 my-24"/>
<slot v-else>
<slot>
<Endpoints
v-show="retrievedData"
:endpointStatuses="endpointStatuses"
:showStatusOnHover="true"
@showTooltip="showTooltip"
@toggleShowAverageResponseTime="toggleShowAverageResponseTime"
:showAverageResponseTime="showAverageResponseTime"
/>
<Pagination @page="changePage"/>
<Pagination v-show="retrievedData" @page="changePage"/>
</slot>
<Settings @refreshData="fetchData"/>
</template>
@@ -31,6 +32,7 @@ export default {
emits: ['showTooltip', 'toggleShowAverageResponseTime'],
methods: {
fetchData() {
this.retrievedData = false;
fetch(`${SERVER_URL}/api/v1/endpoints/statuses?page=${this.currentPage}`, {credentials: 'include'})
.then(response => {
this.retrievedData = true;

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"/><meta name="description" content="{{ .Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="{{ .Title }}"><meta name="application-name" content="{{ .Title }}"><meta name="theme-color" content="#f7f9fb"><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="description" content="{{ .Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .Title }}"/><meta name="application-name" content="{{ .Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long