diff --git a/config/config.go b/config/config.go index 623ecdc3..90945e39 100644 --- a/config/config.go +++ b/config/config.go @@ -41,6 +41,10 @@ var ( // ErrInvalidSecurityConfig is an error returned when the security configuration is invalid ErrInvalidSecurityConfig = errors.New("invalid security configuration") + + // StaticFolder is the path to the location of the static folder from the root path of the project + // The only reason this is exposed is to allow running tests from a different path than the root path of the project + StaticFolder = "./web/static" ) // Config is the main configuration structure @@ -75,6 +79,9 @@ type Config struct { // Web is the configuration for the web listener Web *WebConfig `yaml:"web"` + // UI is the configuration for the UI + UI *UIConfig `yaml:"ui"` + filePath string // path to the file from which config was loaded from lastFileModTime time.Time // last modification time } @@ -162,6 +169,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { if err := validateWebConfig(config); err != nil { return nil, err } + if err := validateUIConfig(config); err != nil { + return nil, err + } if err := validateStorageConfig(config); err != nil { return nil, err } @@ -191,9 +201,20 @@ func validateStorageConfig(config *Config) error { return nil } +func validateUIConfig(config *Config) error { + if config.UI == nil { + config.UI = GetDefaultUIConfig() + } else { + if err := config.UI.validateAndSetDefaults(); err != nil { + return err + } + } + return nil +} + func validateWebConfig(config *Config) error { if config.Web == nil { - config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort} + config.Web = GetDefaultWebConfig() } else { return config.Web.validateAndSetDefaults() } diff --git a/config/config_test.go b/config/config_test.go index 28135e7b..0e6f362f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -36,10 +36,15 @@ func TestLoadDefaultConfigurationFile(t *testing.T) { func TestParseAndValidateConfigBytes(t *testing.T) { file := t.TempDir() + "/test.db" + StaticFolder = "../web/static" + defer func() { + StaticFolder = "./web/static" + }() config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` storage: file: %s - +ui: + title: Test services: - name: twinnation url: https://twinnation.org/health @@ -71,6 +76,9 @@ services: if config == nil { t.Fatal("Config shouldn't have been nil") } + if config.UI == nil || config.UI.Title != "Test" { + t.Error("Expected Config.UI.Title to be Test") + } if len(config.Services) != 3 { t.Error("Should have returned two services") } diff --git a/config/ui.go b/config/ui.go new file mode 100644 index 00000000..0d2e50fd --- /dev/null +++ b/config/ui.go @@ -0,0 +1,36 @@ +package config + +import ( + "bytes" + "html/template" +) + +const defaultTitle = "Health Dashboard | Gatus" + +// UIConfig is the configuration for the UI of Gatus +type UIConfig struct { + Title string `yaml:"title"` // Title of the page +} + +// GetDefaultUIConfig returns a UIConfig struct with the default values +func GetDefaultUIConfig() *UIConfig { + return &UIConfig{ + Title: defaultTitle, + } +} + +func (cfg *UIConfig) validateAndSetDefaults() error { + if len(cfg.Title) == 0 { + cfg.Title = defaultTitle + } + t, err := template.ParseFiles(StaticFolder + "/index.html") + if err != nil { + return err + } + var buffer bytes.Buffer + err = t.Execute(&buffer, cfg) + if err != nil { + return err + } + return nil +} diff --git a/config/ui_test.go b/config/ui_test.go new file mode 100644 index 00000000..c956a4b3 --- /dev/null +++ b/config/ui_test.go @@ -0,0 +1,21 @@ +package config + +import "testing" + +func TestUIConfig_validateAndSetDefaults(t *testing.T) { + StaticFolder = "../web/static" + defer func() { + StaticFolder = "./web/static" + }() + uiConfig := &UIConfig{Title: ""} + if err := uiConfig.validateAndSetDefaults(); err != nil { + t.Error("expected no error, got", err.Error()) + } +} + +func TestGetDefaultUIConfig(t *testing.T) { + defaultUIConfig := GetDefaultUIConfig() + if defaultUIConfig.Title != defaultTitle { + t.Error("expected GetDefaultUIConfig() to return defaultTitle, got", defaultUIConfig.Title) + } +} diff --git a/config/web.go b/config/web.go index 55adf3ef..01543c16 100644 --- a/config/web.go +++ b/config/web.go @@ -15,6 +15,11 @@ type WebConfig struct { Port int `yaml:"port"` } +// GetDefaultWebConfig returns a WebConfig struct with the default values +func GetDefaultWebConfig() *WebConfig { + return &WebConfig{Address: DefaultAddress, Port: DefaultPort} +} + // validateAndSetDefaults checks and sets the default values for fields that are not set func (web *WebConfig) validateAndSetDefaults() error { // Validate the Address diff --git a/controller/controller.go b/controller/controller.go index 9a89c835..97d7b25e 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -30,18 +30,14 @@ const ( var ( cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.FirstInFirstOut) - // staticFolder is the path to the location of the static folder from the root path of the project - // The only reason this is exposed is to allow running tests from a different path than the root path of the project - staticFolder = "./web/static" - // server is the http.Server created by Handle. // The only reason it exists is for testing purposes. server *http.Server ) // Handle creates the router and starts the server -func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) { - var router http.Handler = CreateRouter(securityConfig, enableMetrics) +func Handle(securityConfig *security.Config, webConfig *config.WebConfig, uiConfig *config.UIConfig, enableMetrics bool) { + var router http.Handler = CreateRouter(config.StaticFolder, securityConfig, uiConfig, enableMetrics) if os.Getenv("ENVIRONMENT") == "dev" { router = developmentCorsHandler(router) } @@ -68,14 +64,14 @@ func Shutdown() { } // CreateRouter creates the router for the http server -func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Router { +func CreateRouter(staticFolder string, securityConfig *security.Config, uiConfig *config.UIConfig, enabledMetrics bool) *mux.Router { router := mux.NewRouter() if enabledMetrics { router.Handle("/metrics", promhttp.Handler()).Methods("GET") } router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET") - router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") - // New endpoints + router.HandleFunc("/favicon.ico", favIconHandler(staticFolder)).Methods("GET") + // Endpoints router.HandleFunc("/api/v1/services/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already router.HandleFunc("/api/v1/services/{key}/statuses", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET") // TODO: router.HandleFunc("/api/v1/services/{key}/uptimes", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceUptimesHandler))).Methods("GET") @@ -84,7 +80,8 @@ func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Rou router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/badge.svg", responseTimeBadgeHandler).Methods("GET") router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/chart.svg", responseTimeChartHandler).Methods("GET") // SPA - router.HandleFunc("/services/{service}", spaHandler).Methods("GET") + router.HandleFunc("/services/{service}", spaHandler(staticFolder, uiConfig)).Methods("GET") + router.HandleFunc("/", spaHandler(staticFolder, uiConfig)).Methods("GET") // Everything else falls back on static content router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder)))) return router diff --git a/controller/controller_test.go b/controller/controller_test.go index 0be83f27..f5da26e9 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -88,7 +88,6 @@ var ( func TestCreateRouter(t *testing.T) { defer storage.Get().Clear() defer cache.Clear() - staticFolder = "../web/static" cfg := &config.Config{ Metrics: true, Services: []*core.Service{ @@ -104,7 +103,7 @@ func TestCreateRouter(t *testing.T) { } watchdog.UpdateServiceStatuses(cfg.Services[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) watchdog.UpdateServiceStatuses(cfg.Services[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) - router := CreateRouter(cfg.Security, cfg.Metrics) + router := CreateRouter("../web/static", cfg.Security, nil, cfg.Metrics) type Scenario struct { Name string Path string @@ -287,7 +286,7 @@ func TestHandle(t *testing.T) { _ = os.Setenv("ROUTER_TEST", "true") _ = os.Setenv("ENVIRONMENT", "dev") defer os.Clearenv() - Handle(cfg.Security, cfg.Web, cfg.Metrics) + Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics) defer Shutdown() request, _ := http.NewRequest("GET", "/health", nil) responseRecorder := httptest.NewRecorder() @@ -312,7 +311,6 @@ func TestShutdown(t *testing.T) { func TestServiceStatusesHandler(t *testing.T) { defer storage.Get().Clear() defer cache.Clear() - staticFolder = "../web/static" firstResult := &testSuccessfulResult secondResult := &testUnsuccessfulResult storage.Get().Insert(&testService, firstResult) @@ -320,7 +318,7 @@ func TestServiceStatusesHandler(t *testing.T) { // Can't be bothered dealing with timezone issues on the worker that runs the automated tests firstResult.Timestamp = time.Time{} secondResult.Timestamp = time.Time{} - router := CreateRouter(nil, false) + router := CreateRouter("../web/static", nil, nil, false) type Scenario struct { Name string diff --git a/controller/favicon.go b/controller/favicon.go index 5d10a8d9..afab6a68 100644 --- a/controller/favicon.go +++ b/controller/favicon.go @@ -1,8 +1,12 @@ package controller -import "net/http" +import ( + "net/http" +) // favIconHandler handles requests for /favicon.ico -func favIconHandler(writer http.ResponseWriter, request *http.Request) { - http.ServeFile(writer, request, staticFolder+"/favicon.ico") +func favIconHandler(staticFolder string) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + http.ServeFile(writer, request, staticFolder+"/favicon.ico") + } } diff --git a/controller/spa.go b/controller/spa.go index e44c1112..ae139602 100644 --- a/controller/spa.go +++ b/controller/spa.go @@ -1,8 +1,27 @@ package controller -import "net/http" +import ( + "html/template" + "log" + "net/http" -// spaHandler handles requests for / -func spaHandler(writer http.ResponseWriter, request *http.Request) { - http.ServeFile(writer, request, staticFolder+"/index.html") + "github.com/TwinProduction/gatus/config" +) + +func spaHandler(staticFolder string, ui *config.UIConfig) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + t, err := template.ParseFiles(staticFolder + "/index.html") + if err != nil { + log.Println("[controller][spaHandler] Failed to parse template:", err.Error()) + http.ServeFile(writer, request, staticFolder+"/index.html") + return + } + writer.Header().Set("Content-Type", "text/html") + err = t.Execute(writer, ui) + if err != nil { + log.Println("[controller][spaHandler] Failed to parse template:", err.Error()) + http.ServeFile(writer, request, staticFolder+"/index.html") + return + } + } } diff --git a/main.go b/main.go index c867fb7b..c9f5b01a 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { } func start(cfg *config.Config) { - go controller.Handle(cfg.Security, cfg.Web, cfg.Metrics) + go controller.Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics) watchdog.Monitor(cfg) go listenToConfigurationFileChanges(cfg) } diff --git a/web/app/public/index.html b/web/app/public/index.html index 6a43fac4..f845a286 100644 --- a/web/app/public/index.html +++ b/web/app/public/index.html @@ -2,7 +2,7 @@ - Health Dashboard | Gatus + {{ .Title }} diff --git a/web/static/index.html b/web/static/index.html index aa325469..b06dbee6 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -1 +1 @@ -Health Dashboard | Gatus
\ No newline at end of file +{{ .Title }}
\ No newline at end of file