feat: Bundle assets in binary using go:embed (#340)

Fixes #47
This commit is contained in:
TwiN
2022-10-09 21:33:31 -04:00
committed by GitHub
parent 47dd18a0b5
commit 00b56ecefd
16 changed files with 135 additions and 83 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")
}
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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",

View File

@ -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
}
}

View File

@ -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