Remove web.context-root
This commit is contained in:
		| @ -151,7 +151,6 @@ Note that you can also add environment variables in the configuration file (i.e. | |||||||
| | `web`                                    | Web configuration                                                             | `{}`           | | | `web`                                    | Web configuration                                                             | `{}`           | | ||||||
| | `web.address`                            | Address to listen on                                                          | `0.0.0.0`      | | | `web.address`                            | Address to listen on                                                          | `0.0.0.0`      | | ||||||
| | `web.port`                               | Port to listen on                                                             | `8080`         | | | `web.port`                               | Port to listen on                                                             | `8080`         | | ||||||
| | `web.context-root`                       | Context root at which Gatus will be exposed (frontend and backend)            | `/`            | |  | ||||||
|  |  | ||||||
| For Kubernetes configuration, see [Kubernetes](#kubernetes-alpha) | For Kubernetes configuration, see [Kubernetes](#kubernetes-alpha) | ||||||
|  |  | ||||||
|  | |||||||
| @ -28,9 +28,6 @@ const ( | |||||||
|  |  | ||||||
| 	// DefaultPort is the default port the service will listen on | 	// DefaultPort is the default port the service will listen on | ||||||
| 	DefaultPort = 8080 | 	DefaultPort = 8080 | ||||||
|  |  | ||||||
| 	// DefaultContextRoot is the default context root of the web application |  | ||||||
| 	DefaultContextRoot = "/" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @ -153,7 +150,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { | |||||||
|  |  | ||||||
| func validateWebConfig(config *Config) { | func validateWebConfig(config *Config) { | ||||||
| 	if config.Web == nil { | 	if config.Web == nil { | ||||||
| 		config.Web = &webConfig{Address: DefaultAddress, Port: DefaultPort, ContextRoot: DefaultContextRoot} | 		config.Web = &webConfig{Address: DefaultAddress, Port: DefaultPort} | ||||||
| 	} else { | 	} else { | ||||||
| 		config.Web.validateAndSetDefaults() | 		config.Web.validateAndSetDefaults() | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -119,9 +119,6 @@ services: | |||||||
| 	if config.Web.Port != DefaultPort { | 	if config.Web.Port != DefaultPort { | ||||||
| 		t.Errorf("Port should have been %d, because it is the default value", DefaultPort) | 		t.Errorf("Port should have been %d, because it is the default value", DefaultPort) | ||||||
| 	} | 	} | ||||||
| 	if config.Web.ContextRoot != DefaultContextRoot { |  | ||||||
| 		t.Errorf("ContextRoot should have been %s, because it is the default value", DefaultContextRoot) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestParseAndValidateConfigBytesWithAddress(t *testing.T) { | func TestParseAndValidateConfigBytesWithAddress(t *testing.T) { | ||||||
| @ -226,44 +223,6 @@ services: | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestParseAndValidateConfigBytesWithPortAndHostAndContextRoot(t *testing.T) { |  | ||||||
| 	config, err := parseAndValidateConfigBytes([]byte(` |  | ||||||
| web: |  | ||||||
|   port: 12345 |  | ||||||
|   address: 127.0.0.1 |  | ||||||
|   context-root: /deeply/nested/down=/their |  | ||||||
| services: |  | ||||||
|   - name: twinnation |  | ||||||
|     url: https://twinnation.org/health |  | ||||||
|     conditions: |  | ||||||
|       - "[STATUS] == 200" |  | ||||||
| `)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Error("No error should've been returned") |  | ||||||
| 	} |  | ||||||
| 	if config == nil { |  | ||||||
| 		t.Fatal("Config shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if config.Metrics { |  | ||||||
| 		t.Error("Metrics should've been false by default") |  | ||||||
| 	} |  | ||||||
| 	if config.Services[0].URL != "https://twinnation.org/health" { |  | ||||||
| 		t.Errorf("URL should have been %s", "https://twinnation.org/health") |  | ||||||
| 	} |  | ||||||
| 	if config.Services[0].Interval != 60*time.Second { |  | ||||||
| 		t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) |  | ||||||
| 	} |  | ||||||
| 	if config.Web.Address != "127.0.0.1" { |  | ||||||
| 		t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1") |  | ||||||
| 	} |  | ||||||
| 	if config.Web.Port != 12345 { |  | ||||||
| 		t.Errorf("Port should have been %d, because it is specified in config", 12345) |  | ||||||
| 	} |  | ||||||
| 	if config.Web.ContextRoot != "/deeply/nested/down=/their/" { |  | ||||||
| 		t.Errorf("Port should have been %s, because it is specified in config", "/deeply/nested/down=/their/") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) { | func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) { | ||||||
| 	defer func() { recover() }() | 	defer func() { recover() }() | ||||||
| 	_, _ = parseAndValidateConfigBytes([]byte(` | 	_, _ = parseAndValidateConfigBytes([]byte(` | ||||||
| @ -311,9 +270,6 @@ services: | |||||||
| 	if config.Web.Port != DefaultPort { | 	if config.Web.Port != DefaultPort { | ||||||
| 		t.Errorf("Port should have been %d, because it is the default value", DefaultPort) | 		t.Errorf("Port should have been %d, because it is the default value", DefaultPort) | ||||||
| 	} | 	} | ||||||
| 	if config.Web.ContextRoot != DefaultContextRoot { |  | ||||||
| 		t.Errorf("ContextRoot should have been %s, because it is the default value", DefaultContextRoot) |  | ||||||
| 	} |  | ||||||
| 	if userAgent := config.Services[0].Headers["User-Agent"]; userAgent != "Test/2.0" { | 	if userAgent := config.Services[0].Headers["User-Agent"]; userAgent != "Test/2.0" { | ||||||
| 		t.Errorf("User-Agent should've been %s, got %s", "Test/2.0", userAgent) | 		t.Errorf("User-Agent should've been %s, got %s", "Test/2.0", userAgent) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -3,8 +3,6 @@ package config | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"math" | 	"math" | ||||||
| 	"net/url" |  | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // webConfig is the structure which supports the configuration of the endpoint | // webConfig is the structure which supports the configuration of the endpoint | ||||||
| @ -15,9 +13,6 @@ type webConfig struct { | |||||||
|  |  | ||||||
| 	// Port to listen on (default to 8080 specified by DefaultPort) | 	// Port to listen on (default to 8080 specified by DefaultPort) | ||||||
| 	Port int `yaml:"port"` | 	Port int `yaml:"port"` | ||||||
|  |  | ||||||
| 	// ContextRoot set the root context for the web application |  | ||||||
| 	ContextRoot string `yaml:"context-root"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // validateAndSetDefaults checks and sets the default values for fields that are not set | // validateAndSetDefaults checks and sets the default values for fields that are not set | ||||||
| @ -32,32 +27,9 @@ func (web *webConfig) validateAndSetDefaults() { | |||||||
| 	} else if web.Port < 0 || web.Port > math.MaxUint16 { | 	} else if web.Port < 0 || web.Port > math.MaxUint16 { | ||||||
| 		panic(fmt.Sprintf("invalid port: value should be between %d and %d", 0, math.MaxUint16)) | 		panic(fmt.Sprintf("invalid port: value should be between %d and %d", 0, math.MaxUint16)) | ||||||
| 	} | 	} | ||||||
| 	// Validate the ContextRoot |  | ||||||
| 	if len(web.ContextRoot) == 0 { |  | ||||||
| 		web.ContextRoot = DefaultContextRoot |  | ||||||
| 	} else { |  | ||||||
| 		trimmedContextRoot := strings.Trim(web.ContextRoot, "/") |  | ||||||
| 		if len(trimmedContextRoot) == 0 { |  | ||||||
| 			web.ContextRoot = DefaultContextRoot |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		rootContextURL, err := url.Parse(trimmedContextRoot) |  | ||||||
| 		if err != nil { |  | ||||||
| 			panic("invalid context root:" + err.Error()) |  | ||||||
| 		} |  | ||||||
| 		if rootContextURL.Path != trimmedContextRoot { |  | ||||||
| 			panic("invalid context root: too complex") |  | ||||||
| 		} |  | ||||||
| 		web.ContextRoot = "/" + strings.Trim(rootContextURL.Path, "/") + "/" |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // SocketAddress returns the combination of the Address and the Port | // SocketAddress returns the combination of the Address and the Port | ||||||
| func (web *webConfig) SocketAddress() string { | func (web *webConfig) SocketAddress() string { | ||||||
| 	return fmt.Sprintf("%s:%d", web.Address, web.Port) | 	return fmt.Sprintf("%s:%d", web.Address, web.Port) | ||||||
| } | } | ||||||
|  |  | ||||||
| // PrependWithContextRoot appends the given path to the ContextRoot |  | ||||||
| func (web *webConfig) PrependWithContextRoot(path string) string { |  | ||||||
| 	return web.ContextRoot + strings.Trim(path, "/") |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package config | package config | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -14,86 +13,3 @@ func TestWebConfig_SocketAddress(t *testing.T) { | |||||||
| 		t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress()) | 		t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestWebConfig_PrependWithContextRoot(t *testing.T) { |  | ||||||
| 	web := &webConfig{ContextRoot: "/status/"} |  | ||||||
| 	if result := web.PrependWithContextRoot("/api/v1/results"); result != "/status/api/v1/results" { |  | ||||||
| 		t.Errorf("expected %s, got %s", "/status/api/v1/results", result) |  | ||||||
| 	} |  | ||||||
| 	if result := web.PrependWithContextRoot("/health"); result != "/status/health" { |  | ||||||
| 		t.Errorf("expected %s, got %s", "/status/health", result) |  | ||||||
| 	} |  | ||||||
| 	if result := web.PrependWithContextRoot("/health/"); result != "/status/health" { |  | ||||||
| 		t.Errorf("expected %s, got %s", "/status/health", result) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // validContextRootTest specifies all test case which should end up in |  | ||||||
| // a valid context root used to bind the web interface to |  | ||||||
| var validContextRootTests = []struct { |  | ||||||
| 	name         string |  | ||||||
| 	path         string |  | ||||||
| 	expectedPath string |  | ||||||
| }{ |  | ||||||
| 	{"Empty", "", "/"}, |  | ||||||
| 	{"/", "/", "/"}, |  | ||||||
| 	{"///", "///", "/"}, |  | ||||||
| 	{"Single character 'a'", "a", "/a/"}, |  | ||||||
| 	{"Slash at the beginning", "/status", "/status/"}, |  | ||||||
| 	{"Slashes at start and end", "/status/", "/status/"}, |  | ||||||
| 	{"Multiple slashes at start", "//status", "/status/"}, |  | ||||||
| 	{"Multiple slashes at start and end", "///status////", "/status/"}, |  | ||||||
| 	{"Contains '@' in path'", "me@/status/gatus", "/me@/status/gatus/"}, |  | ||||||
| 	{"Nested context with trailing slash", "/status/gatus/", "/status/gatus/"}, |  | ||||||
| 	{"Nested context without trailing slash", "/status/gatus/system", "/status/gatus/system/"}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestWebConfig_ValidContextRoots(t *testing.T) { |  | ||||||
| 	for idx, test := range validContextRootTests { |  | ||||||
| 		t.Run(fmt.Sprintf("%d: %s", idx, test.name), func(t *testing.T) { |  | ||||||
| 			expectValidResultForContextRoot(t, test.path, test.expectedPath) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func expectValidResultForContextRoot(t *testing.T, path string, expected string) { |  | ||||||
| 	web := &webConfig{ |  | ||||||
| 		ContextRoot: path, |  | ||||||
| 	} |  | ||||||
| 	web.validateAndSetDefaults() |  | ||||||
| 	if web.ContextRoot != expected { |  | ||||||
| 		t.Errorf("expected %s, got %s", expected, web.ContextRoot) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // invalidContextRootTests contains all tests for context root which are |  | ||||||
| // expected to fail and stop program execution |  | ||||||
| var invalidContextRootTests = []struct { |  | ||||||
| 	name string |  | ||||||
| 	path string |  | ||||||
| }{ |  | ||||||
| 	{"Only a fragment identifier", "#"}, |  | ||||||
| 	{"Invalid character in path", "/invalid" + string([]byte{0x7F})}, |  | ||||||
| 	{"Starts with protocol", "http://status/gatus"}, |  | ||||||
| 	{"Path with fragment", "/status/gatus#here"}, |  | ||||||
| 	{"Starts with '://'", "://status"}, |  | ||||||
| 	{"Contains query parameter", "/status/h?ello=world"}, |  | ||||||
| 	{"Contains '?'", "/status?"}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestWebConfig_InvalidContextRoots(t *testing.T) { |  | ||||||
| 	for idx, test := range invalidContextRootTests { |  | ||||||
| 		t.Run(fmt.Sprintf("%d: %s", idx, test.name), func(t *testing.T) { |  | ||||||
| 			expectInvalidResultForContextRoot(t, test.path) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func expectInvalidResultForContextRoot(t *testing.T, path string) { |  | ||||||
| 	defer func() { recover() }() |  | ||||||
|  |  | ||||||
| 	web := &webConfig{ContextRoot: path} |  | ||||||
| 	web.validateAndSetDefaults() |  | ||||||
|  |  | ||||||
| 	t.Fatal(fmt.Sprintf("Should've panicked because the configuration specifies an invalid context root: %s", path)) |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ func Handle() { | |||||||
| 		WriteTimeout: 15 * time.Second, | 		WriteTimeout: 15 * time.Second, | ||||||
| 		IdleTimeout:  15 * time.Second, | 		IdleTimeout:  15 * time.Second, | ||||||
| 	} | 	} | ||||||
| 	log.Printf("[controller][Handle] Listening on %s%s\n", cfg.Web.SocketAddress(), cfg.Web.ContextRoot) | 	log.Println("[controller][Handle] Listening on" + cfg.Web.SocketAddress()) | ||||||
| 	log.Fatal(server.ListenAndServe()) | 	log.Fatal(server.ListenAndServe()) | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -56,13 +56,13 @@ func CreateRouter(cfg *config.Config) *mux.Router { | |||||||
| 	router := mux.NewRouter() | 	router := mux.NewRouter() | ||||||
| 	router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root | 	router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root | ||||||
| 	router.HandleFunc("/services/{service}", spaHandler).Methods("GET") | 	router.HandleFunc("/services/{service}", spaHandler).Methods("GET") | ||||||
| 	router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET") | 	router.HandleFunc("/api/v1/statuses", secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET") | ||||||
| 	router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET") | 	router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET") | ||||||
| 	router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET") | 	router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET") | ||||||
| 	router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET") | 	router.HandleFunc("/health", healthHandler).Methods("GET") | ||||||
| 	router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./web/static"))))) | 	router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir("./web/static")))) | ||||||
| 	if cfg.Metrics { | 	if cfg.Metrics { | ||||||
| 		router.Handle(cfg.Web.PrependWithContextRoot("/metrics"), promhttp.Handler()).Methods("GET") | 		router.Handle("/metrics", promhttp.Handler()).Methods("GET") | ||||||
| 	} | 	} | ||||||
| 	return router | 	return router | ||||||
| } | } | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ export default { | |||||||
|       if (!this.data) { |       if (!this.data) { | ||||||
|         return '/'; |         return '/'; | ||||||
|       } |       } | ||||||
|       return '/services/' + this.data.key; |       return `/services/${this.data.key}`; | ||||||
|     }, |     }, | ||||||
|     showTooltip(result, event) { |     showTooltip(result, event) { | ||||||
|       this.$emit('showTooltip', result, event); |       this.$emit('showTooltip', result, event); | ||||||
|  | |||||||
| @ -1,23 +1,23 @@ | |||||||
| import { createRouter, createWebHistory } from 'vue-router' | import {createRouter, createWebHistory} from 'vue-router' | ||||||
| import Home from '../views/Home.vue' | import Home from '@/views/Home' | ||||||
| import Details from "@/views/Details"; | import Details from "@/views/Details"; | ||||||
|  |  | ||||||
| const routes = [ | const routes = [ | ||||||
|   { | 	{ | ||||||
|     path: '/', | 		path: '/', | ||||||
|     name: 'Home', | 		name: 'Home', | ||||||
|     component: Home | 		component: Home | ||||||
|   }, | 	}, | ||||||
|   { | 	{ | ||||||
|     path: '/services/:key', | 		path: '/services/:key', | ||||||
|     name: 'Details', | 		name: 'Details', | ||||||
|     component: Details | 		component: Details, | ||||||
|   } | 	}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|   history: createWebHistory(process.env.BASE_URL), | 	history: createWebHistory(process.env.BASE_URL), | ||||||
|   routes | 	routes | ||||||
| }) | }) | ||||||
|  |  | ||||||
| export default router | export default router | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <router-link to="/" class="absolute top-2 left-2 inline-block px-2 py-0 text-lg text-black transition bg-gray-100 rounded shadow ripple hover:shadow-lg hover:bg-gray-200 focus:outline-none"> |   <router-link to="../" class="absolute top-2 left-2 inline-block px-2 py-0 text-lg text-black transition bg-gray-100 rounded shadow ripple hover:shadow-lg hover:bg-gray-200 focus:outline-none"> | ||||||
|     ← |     ← | ||||||
|   </router-link> |   </router-link> | ||||||
|   <div class="container mx-auto"> |   <div class="container mx-auto"> | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
| 	filenameHashing: false, | 	filenameHashing: false, | ||||||
| 	productionSourceMap: false, | 	productionSourceMap: false, | ||||||
| 	outputDir: '../static' | 	outputDir: '../static', | ||||||
|  | 	publicPath: '/' | ||||||
| } | } | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user