From 9039ef79cf21a1d8381095cc0c8b8b30584f2150 Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Tue, 17 Nov 2020 23:53:57 +0100 Subject: [PATCH 1/6] add parameterization for host and port Host and port can be specified by either a environment variable or a dedicated command line parameter. Command line parameters will be preferred over environment variables, which will be preferred over the application defaults. --- main.go | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 0d3ffcb9..9427307e 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,12 @@ package main import ( "bytes" "compress/gzip" + "flag" + "fmt" "log" "net/http" "os" + "strconv" "strings" "time" @@ -21,9 +24,41 @@ var ( cachedServiceResults []byte cachedServiceResultsGzipped []byte cachedServiceResultsTimestamp time.Time + port int + host string ) +func init() { + // Customizing priority: + // (1) command line parameters will be preferred over + // (2) environment variables will be preferred over + // (3) application defaults + + // set defaults for the case that neither an environment variable nor a + // command line parameter is passed + var defaultHost = "" + var defaultPort = 8080 + + // assume set if the is a valid port number + if p, err := strconv.Atoi(os.Getenv("GATUS_CONFIG_PORT")); err == nil && p > 0 { + defaultPort = p + } + + // explicitly asked if the user has set a the environment variable to + // blank / empty in order to allow listening on all interfaces + if h, set := os.LookupEnv("GATUS_CONFIG_HOST"); set == true { + defaultHost = h + } + + flag.IntVar(&port, "port", defaultPort, "port to listen (default: 8080)") + flag.IntVar(&port, "p", defaultPort, "port to listen (default: 8080 ; shorthand)") + flag.StringVar(&host, "host", defaultHost, "host to listen on (default all interfaces on host)") + flag.StringVar(&host, "host", defaultHost, "host to listen on (default all interfaces on host; shorthand)") +} + func main() { + flag.Parse() + cfg := loadConfiguration() resultsHandler := serviceResultsHandler if cfg.Security != nil && cfg.Security.IsValid() { @@ -35,9 +70,10 @@ func main() { if cfg.Metrics { http.Handle("/metrics", promhttp.Handler()) } - log.Println("[main][main] Listening on port 8080") + + log.Printf("[main][main] Listening on %s:%d", host, port) go watchdog.Monitor(cfg) - log.Fatal(http.ListenAndServe(":8080", nil)) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), nil)) } func loadConfiguration() *config.Config { From 9f485b14e08fabcc44469e7b06d5a582c8914356 Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Wed, 18 Nov 2020 00:46:37 +0100 Subject: [PATCH 2/6] fix shortcut for host --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 9427307e..66060dca 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func init() { flag.IntVar(&port, "port", defaultPort, "port to listen (default: 8080)") flag.IntVar(&port, "p", defaultPort, "port to listen (default: 8080 ; shorthand)") flag.StringVar(&host, "host", defaultHost, "host to listen on (default all interfaces on host)") - flag.StringVar(&host, "host", defaultHost, "host to listen on (default all interfaces on host; shorthand)") + flag.StringVar(&host, "h", defaultHost, "host to listen on (default all interfaces on host; shorthand)") } func main() { From 10310cf3804eec9129efef38ffdb8aa32d7afac7 Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Thu, 19 Nov 2020 19:39:48 +0100 Subject: [PATCH 3/6] Add configuration values for address and port Add (non-mandatory) configuration values to set address and port on which the web frontent will be served. If not set defaults will be applied. --- .gitignore | 3 +- README.md | 2 + config/config.go | 45 ++++++++++++ config/config_test.go | 159 ++++++++++++++++++++++++++++++++++++++++++ main.go | 35 +--------- 5 files changed, 210 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index fae3642c..48ae5ece 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin -.idea \ No newline at end of file +.idea +.vscode diff --git a/README.md b/README.md index 81c65eed..5abba02c 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ Note that you can also add environment variables in the configuration file (i.e. | `security.basic.username` | Username for Basic authentication | Required `""` | | `security.basic.password-sha512` | Password's SHA512 hash for Basic authentication | Required `""` | | `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock) | `false` | +| `web.address` | Address to listen on | `0.0.0.0` | +| `web.port` | Port to listen on | `8080` | For Kubernetes configuration, see [Kubernetes](#kubernetes-alpha) diff --git a/config/config.go b/config/config.go index cf466c87..57e41ae9 100644 --- a/config/config.go +++ b/config/config.go @@ -2,8 +2,10 @@ package config import ( "errors" + "fmt" "io/ioutil" "log" + "math" "os" "github.com/TwinProduction/gatus/alerting" @@ -22,6 +24,12 @@ const ( // DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the // configuration file if DefaultConfigurationFilePath didn't work DefaultFallbackConfigurationFilePath = "config/config.yml" + + // DefaultAddress is the default address the service will bind to + DefaultAddress = "0.0.0.0" + + // DefaultPort is the default port the service will listen on + DefaultPort = 8080 ) var ( @@ -64,6 +72,9 @@ type Config struct { // Kubernetes is the Kubernetes configuration Kubernetes *k8s.Config `yaml:"kubernetes"` + + // webConfig is the optional configuration of the web listener providing the frontend UI + Web *webConfig `yaml:"web"` } // Get returns the configuration, or panics if the configuration hasn't loaded yet @@ -127,10 +138,20 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { validateSecurityConfig(config) validateServicesConfig(config) validateKubernetesConfig(config) + validateAddressAndPortConfig(config) } return } +func validateAddressAndPortConfig(config *Config) { + if config.Web == nil { + config.Web = &webConfig{Address: DefaultAddress, Port: DefaultPort} + } else { + config.Web.validateAndSetDefaults() + } + +} + func validateKubernetesConfig(config *Config) { if config.Kubernetes != nil && config.Kubernetes.AutoDiscover { if config.Kubernetes.ServiceTemplate == nil { @@ -237,3 +258,27 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr } return nil } + +// webConfig is the structure which supports the configuration of the endpoint +// which provides access to the web frontend +type webConfig struct { + // Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress) + Address string `yaml:"address"` + + // Port to listen on (default to 8080 specified by DefaultPort) + Port int `yaml:"port"` +} + +// validateAndSetDefaults checks and sets missing values based on the defaults +// in given in DefaultAddress and DefaultPort if necessary +func (web *webConfig) validateAndSetDefaults() { + if len(web.Address) == 0 { + web.Address = DefaultAddress + } + + if web.Port == 0 { + web.Port = DefaultPort + } else if web.Port < 0 || web.Port > math.MaxUint16 { + panic(fmt.Sprintf("port has an invalid value %d shoud be between %d - %d\r\n", web.Port, 0, math.MaxUint16)) + } +} diff --git a/config/config_test.go b/config/config_test.go index d9dd8fc7..61952d9d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -103,6 +103,120 @@ services: 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 != DefaultAddress { + t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress) + } + + if config.Web.Port != DefaultPort { + t.Errorf("Port should have been %d, because it is the default value", DefaultPort) + } +} + +func TestParseAndValidateConfigBytesWithAddress(t *testing.T) { + config, err := parseAndValidateConfigBytes([]byte(` +web: + address: 127.0.0.1 +services: + - name: twinnation + url: https://twinnation.org/actuator/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/actuator/health" { + t.Errorf("URL should have been %s", "https://twinnation.org/actuator/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 != DefaultPort { + t.Errorf("Port should have been %d, because it is the default value", DefaultPort) + } +} + +func TestParseAndValidateConfigBytesWithPort(t *testing.T) { + config, err := parseAndValidateConfigBytes([]byte(` +web: + port: 12345 +services: + - name: twinnation + url: https://twinnation.org/actuator/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/actuator/health" { + t.Errorf("URL should have been %s", "https://twinnation.org/actuator/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 != DefaultAddress { + t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress) + } + + if config.Web.Port != 12345 { + t.Errorf("Port should have been %d, because it is specified in config", 12345) + } +} + +func TestParseAndValidateConfigBytesWithPortAndHost(t *testing.T) { + config, err := parseAndValidateConfigBytes([]byte(` +web: + port: 12345 + address: 127.0.0.1 +services: + - name: twinnation + url: https://twinnation.org/actuator/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/actuator/health" { + t.Errorf("URL should have been %s", "https://twinnation.org/actuator/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) + } } func TestParseAndValidateConfigBytesWithMetrics(t *testing.T) { @@ -129,6 +243,51 @@ services: 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 != DefaultAddress { + t.Errorf("Bind address should have been %s, because it is specified in config", DefaultAddress) + } + + if config.Web.Port != DefaultPort { + t.Errorf("Port should have been %d, because it is specified in config", DefaultPort) + } +} + +func TestParseAndValidateConfigBytesWithMetricsAndHostAndPort(t *testing.T) { + config, err := parseAndValidateConfigBytes([]byte(` +metrics: true +services: + - name: twinnation + url: https://twinnation.org/actuator/health + conditions: + - "[STATUS] == 200" +web: + address: 192.168.0.1 + port: 9090 +`)) + 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 have been true") + } + if config.Services[0].URL != "https://twinnation.org/actuator/health" { + t.Errorf("URL should have been %s", "https://twinnation.org/actuator/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 != "192.168.0.1" { + t.Errorf("Bind address should have been %s, because it is the default value", "192.168.0.1") + } + + if config.Web.Port != 9090 { + t.Errorf("Port should have been %d, because it is specified in config", 9090) + } } func TestParseAndValidateBadConfigBytes(t *testing.T) { diff --git a/main.go b/main.go index 66060dca..a566d54d 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,6 @@ import ( "log" "net/http" "os" - "strconv" "strings" "time" @@ -24,38 +23,8 @@ var ( cachedServiceResults []byte cachedServiceResultsGzipped []byte cachedServiceResultsTimestamp time.Time - port int - host string ) -func init() { - // Customizing priority: - // (1) command line parameters will be preferred over - // (2) environment variables will be preferred over - // (3) application defaults - - // set defaults for the case that neither an environment variable nor a - // command line parameter is passed - var defaultHost = "" - var defaultPort = 8080 - - // assume set if the is a valid port number - if p, err := strconv.Atoi(os.Getenv("GATUS_CONFIG_PORT")); err == nil && p > 0 { - defaultPort = p - } - - // explicitly asked if the user has set a the environment variable to - // blank / empty in order to allow listening on all interfaces - if h, set := os.LookupEnv("GATUS_CONFIG_HOST"); set == true { - defaultHost = h - } - - flag.IntVar(&port, "port", defaultPort, "port to listen (default: 8080)") - flag.IntVar(&port, "p", defaultPort, "port to listen (default: 8080 ; shorthand)") - flag.StringVar(&host, "host", defaultHost, "host to listen on (default all interfaces on host)") - flag.StringVar(&host, "h", defaultHost, "host to listen on (default all interfaces on host; shorthand)") -} - func main() { flag.Parse() @@ -71,9 +40,9 @@ func main() { http.Handle("/metrics", promhttp.Handler()) } - log.Printf("[main][main] Listening on %s:%d", host, port) + log.Printf("[main][main] Listening on %s:%d\r\n", cfg.Web.Address, cfg.Web.Port) go watchdog.Monitor(cfg) - log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), nil)) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port), nil)) } func loadConfiguration() *config.Config { From 4c151bdf8fbaa3743d8772430bcf41994db3da4d Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Thu, 19 Nov 2020 20:03:30 +0100 Subject: [PATCH 4/6] add test for invalid port in config file --- config/config_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/config/config_test.go b/config/config_test.go index 61952d9d..d1a4a428 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -219,6 +219,23 @@ services: } } +func TestParseAndValidateConfigBytesWithInvalidPort(t *testing.T) { + defer func() { recover() }() + + parseAndValidateConfigBytes([]byte(` +web: + port: 65536 + address: 127.0.0.1 +services: + - name: twinnation + url: https://twinnation.org/actuator/health + conditions: + - "[STATUS] == 200" +`)) + + t.Fatal("Should've panicked because the configuration specifies an invalid port value") +} + func TestParseAndValidateConfigBytesWithMetrics(t *testing.T) { config, err := parseAndValidateConfigBytes([]byte(` metrics: true From 3985c6c4831a06de9fb4f3c026a5971888728c6f Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Fri, 20 Nov 2020 06:48:23 +0100 Subject: [PATCH 5/6] removed remaining flag parsing --- main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/main.go b/main.go index a566d54d..e7378f97 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "bytes" "compress/gzip" - "flag" "fmt" "log" "net/http" @@ -26,8 +25,6 @@ var ( ) func main() { - flag.Parse() - cfg := loadConfiguration() resultsHandler := serviceResultsHandler if cfg.Security != nil && cfg.Security.IsValid() { From 4dd57b45e9ed083b8891cd471d14e0442eb92db8 Mon Sep 17 00:00:00 2001 From: Michael Engelhardt Date: Fri, 20 Nov 2020 06:59:43 +0100 Subject: [PATCH 6/6] fix misleading messages on failing tests --- config/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index d1a4a428..fb650f7f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -262,11 +262,11 @@ services: } if config.Web.Address != DefaultAddress { - t.Errorf("Bind address should have been %s, because it is specified in config", DefaultAddress) + t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress) } if config.Web.Port != DefaultPort { - t.Errorf("Port should have been %d, because it is specified in config", DefaultPort) + t.Errorf("Port should have been %d, because it is the default value", DefaultPort) } }