mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
parent
47dd18a0b5
commit
00b56ecefd
16 changed files with 135 additions and 83 deletions
|
@ -13,7 +13,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus
|
|||
FROM scratch
|
||||
COPY --from=builder /app/gatus .
|
||||
COPY --from=builder /app/config.yaml ./config/config.yaml
|
||||
COPY --from=builder /app/web/static ./web/static
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ENV PORT=8080
|
||||
EXPOSE ${PORT}
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/TwiN/gatus/v4/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/config/web"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage"
|
||||
|
@ -39,10 +38,6 @@ func TestLoadDefaultConfigurationFile(t *testing.T) {
|
|||
|
||||
func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
ui.StaticFolder = "../web/static"
|
||||
defer func() {
|
||||
ui.StaticFolder = "./web/static"
|
||||
}()
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||
storage:
|
||||
type: sqlite
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"html/template"
|
||||
|
||||
"github.com/TwiN/gatus/v4/web"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -14,10 +16,6 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
// 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"
|
||||
|
||||
ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
|
||||
)
|
||||
|
||||
|
@ -71,7 +69,7 @@ func (cfg *Config) ValidateAndSetDefaults() error {
|
|||
}
|
||||
}
|
||||
// Validate that the template works
|
||||
t, err := template.ParseFiles(StaticFolder + "/index.html")
|
||||
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,10 +6,6 @@ import (
|
|||
)
|
||||
|
||||
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
StaticFolder = "../../web/static"
|
||||
defer func() {
|
||||
StaticFolder = "./web/static"
|
||||
}()
|
||||
cfg := &Config{
|
||||
Title: "",
|
||||
Header: "",
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/controller/handler"
|
||||
)
|
||||
|
||||
|
@ -21,7 +20,7 @@ var (
|
|||
|
||||
// Handle creates the router and starts the server
|
||||
func Handle(cfg *config.Config) {
|
||||
var router http.Handler = handler.CreateRouter(ui.StaticFolder, cfg)
|
||||
var router http.Handler = handler.CreateRouter(cfg)
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
router = handler.DevelopmentCORS(router)
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestBadge(t *testing.T) {
|
|||
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter("../../web/static", cfg)
|
||||
router := CreateRouter(cfg)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestResponseTimeChart(t *testing.T) {
|
|||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter("../../web/static", cfg)
|
||||
router := CreateRouter(cfg)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
|
|
|
@ -97,7 +97,7 @@ func TestEndpointStatus(t *testing.T) {
|
|||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter("../../web/static", cfg)
|
||||
router := CreateRouter(cfg)
|
||||
|
||||
type Scenario struct {
|
||||
Name string
|
||||
|
@ -153,7 +153,7 @@ func TestEndpointStatuses(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("../../web/static", &config.Config{Metrics: true})
|
||||
router := CreateRouter(&config.Config{Metrics: true})
|
||||
|
||||
type Scenario struct {
|
||||
Name string
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FavIcon handles requests for /favicon.ico
|
||||
func FavIcon(staticFolder string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
)
|
||||
|
||||
func TestFavIcon(t *testing.T) {
|
||||
router := CreateRouter("../../web/static", &config.Config{})
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
ExpectedCode int
|
||||
}
|
||||
scenarios := []Scenario{
|
||||
{
|
||||
Name: "favicon",
|
||||
Path: "/favicon.ico",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,15 +1,17 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/web"
|
||||
"github.com/TwiN/health"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func CreateRouter(staticFolder string, cfg *config.Config) *mux.Router {
|
||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
if cfg.Metrics {
|
||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||
|
@ -35,11 +37,14 @@ func CreateRouter(staticFolder string, cfg *config.Config) *mux.Router {
|
|||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET")
|
||||
// Misc
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
router.HandleFunc("/favicon.ico", FavIcon(staticFolder)).Methods("GET")
|
||||
// SPA
|
||||
router.HandleFunc("/endpoints/{name}", SinglePageApplication(staticFolder, cfg.UI)).Methods("GET")
|
||||
router.HandleFunc("/", SinglePageApplication(staticFolder, cfg.UI)).Methods("GET")
|
||||
router.HandleFunc("/endpoints/{name}", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||
router.HandleFunc("/", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||
// Everything else falls back on static content
|
||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
||||
staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.FS(staticFileSystem))))
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func TestCreateRouter(t *testing.T) {
|
||||
router := CreateRouter("../../web/static", &config.Config{Metrics: true})
|
||||
router := CreateRouter(&config.Config{Metrics: true})
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
|
@ -28,16 +28,32 @@ func TestCreateRouter(t *testing.T) {
|
|||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "scripts",
|
||||
Name: "favicon.ico",
|
||||
Path: "/favicon.ico",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "app.js",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "scripts-gzipped",
|
||||
Name: "app.js-gzipped",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js-gzipped",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "index-redirect",
|
||||
Path: "/index.html",
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/web"
|
||||
)
|
||||
|
||||
func SinglePageApplication(staticFolder string, ui *ui.Config) http.HandlerFunc {
|
||||
func SinglePageApplication(ui *ui.Config) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
t, err := template.ParseFiles(staticFolder + "/index.html")
|
||||
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||
if err != nil {
|
||||
log.Println("[handler][SinglePageApplication] Failed to parse template:", err.Error())
|
||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[handler][SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
http.Error(writer, "Failed to parse template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html")
|
||||
err = t.Execute(writer, ui)
|
||||
if err != nil {
|
||||
log.Println("[handler][SinglePageApplication] Failed to parse template:", err.Error())
|
||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[handler][SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
http.Error(writer, "Failed to execute template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestSinglePageApplication(t *testing.T) {
|
|||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter("../../web/static", cfg)
|
||||
router := CreateRouter(cfg)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
|
|
13
web/static.go
Normal file
13
web/static.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package static
|
||||
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed static
|
||||
FileSystem embed.FS
|
||||
)
|
||||
|
||||
const (
|
||||
RootPath = "static"
|
||||
IndexPath = RootPath + "/index.html"
|
||||
)
|
74
web/static_test.go
Normal file
74
web/static_test.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmbed(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
path string
|
||||
shouldExist bool
|
||||
expectedContainString string
|
||||
}{
|
||||
{
|
||||
path: "index.html",
|
||||
shouldExist: true,
|
||||
expectedContainString: "</body>",
|
||||
},
|
||||
{
|
||||
path: "favicon.ico",
|
||||
shouldExist: true,
|
||||
expectedContainString: "", // not checking because it's an image
|
||||
},
|
||||
{
|
||||
path: "img/logo.svg",
|
||||
shouldExist: true,
|
||||
expectedContainString: "</svg>",
|
||||
},
|
||||
{
|
||||
path: "css/app.css",
|
||||
shouldExist: true,
|
||||
expectedContainString: "background-color",
|
||||
},
|
||||
{
|
||||
path: "js/app.js",
|
||||
shouldExist: true,
|
||||
expectedContainString: "function",
|
||||
},
|
||||
{
|
||||
path: "js/chunk-vendors.js",
|
||||
shouldExist: true,
|
||||
expectedContainString: "function",
|
||||
},
|
||||
{
|
||||
path: "file-that-does-not-exist.html",
|
||||
shouldExist: false,
|
||||
},
|
||||
}
|
||||
staticFileSystem, err := fs.Sub(FileSystem, RootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.path, func(t *testing.T) {
|
||||
content, err := fs.ReadFile(staticFileSystem, scenario.path)
|
||||
if !scenario.shouldExist {
|
||||
if err == nil {
|
||||
t.Errorf("%s should not have existed", scenario.path)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("opening %s should not have returned an error, got %s", scenario.path, err.Error())
|
||||
}
|
||||
if len(content) == 0 {
|
||||
t.Errorf("%s should have existed in the static FileSystem, but was empty", scenario.path)
|
||||
}
|
||||
if !strings.Contains(string(content), scenario.expectedContainString) {
|
||||
t.Errorf("%s should have contained %s, but did not", scenario.path, scenario.expectedContainString)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue