diff --git a/.examples/docker-compose-postgres-storage/config.yaml b/.examples/docker-compose-postgres-storage/config.yaml index 77b9e5bf..de783853 100644 --- a/.examples/docker-compose-postgres-storage/config.yaml +++ b/.examples/docker-compose-postgres-storage/config.yaml @@ -1,6 +1,6 @@ storage: type: postgres - file: "postgres://username:password@postgres:5432/gatus?sslmode=disable" + path: "postgres://username:password@postgres:5432/gatus?sslmode=disable" endpoints: - name: back-end diff --git a/.examples/docker-compose-sqlite-storage/config.yaml b/.examples/docker-compose-sqlite-storage/config.yaml index f43bc4e9..b455c218 100644 --- a/.examples/docker-compose-sqlite-storage/config.yaml +++ b/.examples/docker-compose-sqlite-storage/config.yaml @@ -1,6 +1,6 @@ storage: type: sqlite - file: /data/data.db + path: /data/data.db endpoints: - name: back-end diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba8b03aa..86cff894 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.17 - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Build binary to make sure it works @@ -25,9 +25,9 @@ jobs: - name: Test # We're using "sudo" because one of the tests leverages ping, which requires super-user privileges. # As for the 'env "PATH=$PATH" "GOROOT=$GOROOT"', we need it to use the same "go" executable that - # was configured by the "Set up Go 1.15" step (otherwise, it'd use sudo's "go" executable) + # 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 -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic - name: Codecov - uses: codecov/codecov-action@v1.5.2 + uses: codecov/codecov-action@v2.1.0 with: - file: ./coverage.txt + files: ./coverage.txt diff --git a/README.md b/README.md index 54a34381..0c767af1 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ Here are some examples of conditions you can use: ```yaml storage: type: sqlite - file: data.db + path: data.db ``` See [examples/docker-compose-sqlite-storage](.examples/docker-compose-sqlite-storage) for an example. @@ -252,7 +252,7 @@ See [examples/docker-compose-sqlite-storage](.examples/docker-compose-sqlite-sto ```yaml storage: type: postgres - file: "postgres://user:password@127.0.0.1:5432/gatus?sslmode=disable" + path: "postgres://user:password@127.0.0.1:5432/gatus?sslmode=disable" ``` See [examples/docker-compose-postgres-storage](.examples/docker-compose-postgres-storage) for an example. diff --git a/config/config_test.go b/config/config_test.go index b0ea2ed5..f1bc7cca 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -20,6 +20,7 @@ import ( "github.com/TwiN/gatus/v3/config/ui" "github.com/TwiN/gatus/v3/config/web" "github.com/TwiN/gatus/v3/core" + "github.com/TwiN/gatus/v3/storage" ) func TestLoadFileThatDoesNotExist(t *testing.T) { @@ -44,7 +45,8 @@ func TestParseAndValidateConfigBytes(t *testing.T) { }() config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` storage: - file: %s + type: sqlite + path: %s maintenance: enabled: true start: 00:00 @@ -83,6 +85,9 @@ endpoints: if config == nil { t.Fatal("Config shouldn't have been nil") } + if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite { + t.Error("expected storage to be set to sqlite, got", config.Storage) + } if config.UI == nil || config.UI.Title != "Test" { t.Error("Expected Config.UI.Title to be Test") } @@ -1297,3 +1302,53 @@ endpoints: t.Error("services should've been merged in endpoints") } } + +// XXX: Remove this in v4.0.0 +func TestParseAndValidateConfigBytes_backwardCompatibleWithStorageFile(t *testing.T) { + file := t.TempDir() + "/test.db" + config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` +storage: + type: sqlite + file: %s + +endpoints: + - name: website + url: https://twin.sh/actuator/health + conditions: + - "[STATUS] == 200" +`, file))) + if err != nil { + t.Error("expected no error, got", err.Error()) + } + if config == nil { + t.Fatal("Config shouldn't have been nil") + } + if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite { + t.Error("expected storage to be set to sqlite, got", config.Storage) + } +} + +// XXX: Remove this in v4.0.0 +func TestParseAndValidateConfigBytes_backwardCompatibleWithStorageTypeMemoryAndFile(t *testing.T) { + file := t.TempDir() + "/test.db" + config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` +storage: + type: memory + file: %s + +endpoints: + - name: website + url: https://twin.sh/actuator/health + conditions: + - "[STATUS] == 200" +`, file))) + if err != nil { + t.Error("expected no error, got", err.Error()) + } + if config == nil { + t.Fatal("Config shouldn't have been nil") + } + if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeMemory { + t.Error("expected storage to be set to memory, got", config.Storage) + } +} diff --git a/controller/handler/endpoint_status_test.go b/controller/handler/endpoint_status_test.go index 8cb86b16..f8b88d30 100644 --- a/controller/handler/endpoint_status_test.go +++ b/controller/handler/endpoint_status_test.go @@ -175,37 +175,37 @@ func TestEndpointStatuses(t *testing.T) { Name: "no-pagination", Path: "/api/v1/endpoints/statuses", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`, }, { Name: "pagination-first-result", Path: "/api/v1/endpoints/statuses?page=1&pageSize=1", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`, }, { Name: "pagination-second-result", Path: "/api/v1/endpoints/statuses?page=2&pageSize=1", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]}]`, }, { Name: "pagination-no-results", Path: "/api/v1/endpoints/statuses?page=5&pageSize=20", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[]}]`, }, { Name: "invalid-pagination-should-fall-back-to-default", Path: "/api/v1/endpoints/statuses?page=INVALID&pageSize=INVALID", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`, }, { // XXX: Remove this in v4.0.0 Name: "backward-compatible-service-status", Path: "/api/v1/services/statuses", ExpectedCode: http.StatusOK, - ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"events":[]}]`, + ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`, }, } diff --git a/core/endpoint_status.go b/core/endpoint_status.go index 4a8c8214..b6217880 100644 --- a/core/endpoint_status.go +++ b/core/endpoint_status.go @@ -17,7 +17,7 @@ type EndpointStatus struct { Results []*Result `json:"results"` // Events is a list of events - Events []*Event `json:"events"` + Events []*Event `json:"events,omitempty"` // Uptime information on the endpoint's uptime // diff --git a/storage/config.go b/storage/config.go index 83358b6d..a125637a 100644 --- a/storage/config.go +++ b/storage/config.go @@ -1,17 +1,29 @@ package storage -import "errors" +import ( + "errors" + "log" +) var ( - ErrSQLStorageRequiresFile = errors.New("sql storage requires a non-empty file to be defined") + ErrSQLStorageRequiresFile = errors.New("sql storage requires a non-empty file to be defined") + ErrMemoryStorageDoesNotSupportFile = errors.New("memory storage does not support persistence, use sqlite if you want persistence on file") + ErrCannotSetBothFileAndPath = errors.New("file has been deprecated in favor of path: you cannot set both of them") ) // Config is the configuration for storage type Config struct { + // Path is the path used by the store to achieve persistence + // If blank, persistence is disabled. + // Note that not all Type support persistence + // + // XXX: Rename to path for v4.0.0 + Path string `yaml:"path"` + // File is the path of the file to use for persistence // If blank, persistence is disabled // - // XXX: Rename to path for v4.0.0 + // Deprecated File string `yaml:"file"` // Type of store @@ -21,8 +33,27 @@ type Config struct { // ValidateAndSetDefaults validates the configuration and sets the default values (if applicable) func (c *Config) ValidateAndSetDefaults() error { - if (c.Type == TypePostgres || c.Type == TypeSQLite) && len(c.File) == 0 { + if len(c.File) > 0 && len(c.Path) > 0 { // XXX: Remove for v4.0.0 + return ErrCannotSetBothFileAndPath + } else if len(c.File) > 0 { // XXX: Remove for v4.0.0 + log.Println("WARNING: Your configuration is using 'storage.file', which is deprecated in favor of 'storage.path'") + log.Println("WARNING: storage.file will be completely removed in v4.0.0, so please update your configuration") + log.Println("WARNING: See https://github.com/TwiN/gatus/issues/197") + c.Path = c.File + } + if c.Type == "" { + c.Type = TypeMemory + } + if (c.Type == TypePostgres || c.Type == TypeSQLite) && len(c.Path) == 0 { return ErrSQLStorageRequiresFile } + if c.Type == TypeMemory && len(c.Path) > 0 { + log.Println("WARNING: Your configuration is using a storage of type memory with persistence, which has been deprecated") + log.Println("WARNING: As of v4.0.0, the default storage type (memory) will not support persistence.") + log.Println("WARNING: If you want persistence, use 'storage.type: sqlite' instead of 'storage.type: memory'") + log.Println("WARNING: See https://github.com/TwiN/gatus/issues/198") + // XXX: Uncomment the following line for v4.0.0 + //return ErrMemoryStorageDoesNotSupportFile + } return nil } diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go index cc374d44..867a2c72 100644 --- a/storage/store/memory/memory.go +++ b/storage/store/memory/memory.go @@ -28,6 +28,10 @@ func init() { // Store that leverages gocache type Store struct { sync.RWMutex + // Deprecated + // + // File persistence will no longer be supported as of v4.0.0 + // XXX: Remove me in v4.0.0 file string cache *gocache.Cache } @@ -41,6 +45,8 @@ func NewStore(file string) (*Store, error) { file: file, cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), } + // XXX: Remove the block below in v4.0.0 because persistence with the memory store will no longer be supported + // XXX: Make sure to also update gocache to v2.0.0 if len(file) > 0 { _, err := store.cache.ReadFromFile(file) if err != nil { @@ -57,7 +63,6 @@ func NewStore(file string) (*Store, error) { return store, nil } } - // XXX: Remove the block above in v4.0.0 return nil, err } } diff --git a/storage/store/sql/sql.go b/storage/store/sql/sql.go index b9c8a2b1..644efeab 100644 --- a/storage/store/sql/sql.go +++ b/storage/store/sql/sql.go @@ -34,8 +34,8 @@ const ( ) var ( - // ErrFilePathNotSpecified is the error returned when path parameter passed in NewStore is blank - ErrFilePathNotSpecified = errors.New("file path cannot be empty") + // ErrPathNotSpecified is the error returned when the path parameter passed in NewStore is blank + ErrPathNotSpecified = errors.New("path cannot be empty") // ErrDatabaseDriverNotSpecified is the error returned when the driver parameter passed in NewStore is blank ErrDatabaseDriverNotSpecified = errors.New("database driver cannot be empty") @@ -45,20 +45,20 @@ var ( // Store that leverages a database type Store struct { - driver, file string + driver, path string db *sql.DB } -// NewStore initializes the database and creates the schema if it doesn't already exist in the file specified +// NewStore initializes the database and creates the schema if it doesn't already exist in the path specified func NewStore(driver, path string) (*Store, error) { if len(driver) == 0 { return nil, ErrDatabaseDriverNotSpecified } if len(path) == 0 { - return nil, ErrFilePathNotSpecified + return nil, ErrPathNotSpecified } - store := &Store{driver: driver, file: path} + store := &Store{driver: driver, path: path} var err error if store.db, err = sql.Open(driver, path); err != nil { return nil, err diff --git a/storage/store/sql/sql_test.go b/storage/store/sql/sql_test.go index 35439b22..0389e883 100644 --- a/storage/store/sql/sql_test.go +++ b/storage/store/sql/sql_test.go @@ -84,7 +84,7 @@ func TestNewStore(t *testing.T) { if _, err := NewStore("", "TestNewStore.db"); err != ErrDatabaseDriverNotSpecified { t.Error("expected error due to blank driver parameter") } - if _, err := NewStore("sqlite", ""); err != ErrFilePathNotSpecified { + if _, err := NewStore("sqlite", ""); err != ErrPathNotSpecified { t.Error("expected error due to blank path parameter") } if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db"); err != nil { diff --git a/storage/store/store.go b/storage/store/store.go index 2bb24365..ca6a8c5a 100644 --- a/storage/store/store.go +++ b/storage/store/store.go @@ -94,23 +94,23 @@ func Initialize(cfg *storage.Config) error { if cfg == nil { cfg = &storage.Config{} } - if len(cfg.File) == 0 && cfg.Type != storage.TypePostgres { - log.Printf("[store][Initialize] Creating storage provider with type=%s and file=%s", cfg.Type, cfg.File) + if len(cfg.Path) == 0 && cfg.Type != storage.TypePostgres { + log.Printf("[store][Initialize] Creating storage provider with type=%s and file=%s", cfg.Type, cfg.Path) } else { log.Printf("[store][Initialize] Creating storage provider with type=%s", cfg.Type) } ctx, cancelFunc = context.WithCancel(context.Background()) switch cfg.Type { case storage.TypeSQLite, storage.TypePostgres: - store, err = sql.NewStore(string(cfg.Type), cfg.File) + store, err = sql.NewStore(string(cfg.Type), cfg.Path) if err != nil { return err } case storage.TypeMemory: fallthrough default: - if len(cfg.File) > 0 { - store, err = memory.NewStore(cfg.File) + if len(cfg.Path) > 0 { + store, err = memory.NewStore(cfg.Path) if err != nil { return err } diff --git a/storage/store/store_test.go b/storage/store/store_test.go index fa93bdba..da0d959f 100644 --- a/storage/store/store_test.go +++ b/storage/store/store_test.go @@ -553,17 +553,17 @@ func TestInitialize(t *testing.T) { }, { Name: "memory-with-file", - Cfg: &storage.Config{Type: storage.TypeMemory, File: t.TempDir() + "/TestInitialize_memory-with-file.db"}, + Cfg: &storage.Config{Type: storage.TypeMemory, Path: t.TempDir() + "/TestInitialize_memory-with-file.db"}, ExpectedErr: nil, }, { Name: "sqlite-no-file", Cfg: &storage.Config{Type: storage.TypeSQLite}, - ExpectedErr: sql.ErrFilePathNotSpecified, + ExpectedErr: sql.ErrPathNotSpecified, }, { Name: "sqlite-with-file", - Cfg: &storage.Config{Type: storage.TypeSQLite, File: t.TempDir() + "/TestInitialize_sqlite-with-file.db"}, + Cfg: &storage.Config{Type: storage.TypeSQLite, Path: t.TempDir() + "/TestInitialize_sqlite-with-file.db"}, ExpectedErr: nil, }, } @@ -599,7 +599,7 @@ func TestInitialize(t *testing.T) { func TestAutoSave(t *testing.T) { file := t.TempDir() + "/TestAutoSave.db" - if err := Initialize(&storage.Config{File: file}); err != nil { + if err := Initialize(&storage.Config{Path: file}); err != nil { t.Fatal("shouldn't have returned an error") } go autoSave(ctx, store, 3*time.Millisecond)