diff --git a/go.mod b/go.mod index b9bf086d..e1a00d50 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/prometheus/exporter-toolkit go 1.17 require ( + github.com/coreos/go-systemd/v22 v22.3.2 github.com/go-kit/log v0.2.1 github.com/prometheus/common v0.37.0 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 09e42841..f31fee00 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -118,6 +120,7 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -417,6 +420,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/web/handler_test.go b/web/handler_test.go index 9bd6a506..52f5df8b 100644 --- a/web/handler_test.go +++ b/web/handler_test.go @@ -24,7 +24,6 @@ import ( // protected endpoint multiple times. func TestBasicAuthCache(t *testing.T) { server := &http.Server{ - Addr: port, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) }), @@ -39,7 +38,12 @@ func TestBasicAuthCache(t *testing.T) { }) go func() { - ListenAndServe(server, "testdata/web_config_users_noTLS.good.yml", testlogger) + flags := FlagConfig{ + WebListenAddresses: &([]string{port}), + WebSystemdSocket: OfBool(false), + WebConfigFile: OfString("testdata/web_config_users_noTLS.good.yml"), + } + ListenAndServe(server, &flags, testlogger) close(done) }() @@ -88,7 +92,6 @@ func TestBasicAuthCache(t *testing.T) { // to prevent user enumeration. func TestBasicAuthWithFakepassword(t *testing.T) { server := &http.Server{ - Addr: port, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) }), @@ -103,7 +106,12 @@ func TestBasicAuthWithFakepassword(t *testing.T) { }) go func() { - ListenAndServe(server, "testdata/web_config_users_noTLS.good.yml", testlogger) + flags := FlagConfig{ + WebListenAddresses: &([]string{port}), + WebSystemdSocket: OfBool(false), + WebConfigFile: OfString("testdata/web_config_users_noTLS.good.yml"), + } + ListenAndServe(server, &flags, testlogger) close(done) }() @@ -132,7 +140,6 @@ func TestBasicAuthWithFakepassword(t *testing.T) { // TestHTTPHeaders validates that HTTP headers are added correctly. func TestHTTPHeaders(t *testing.T) { server := &http.Server{ - Addr: port, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) }), @@ -147,7 +154,12 @@ func TestHTTPHeaders(t *testing.T) { }) go func() { - ListenAndServe(server, "testdata/web_config_headers.good.yml", testlogger) + flags := FlagConfig{ + WebListenAddresses: &([]string{port}), + WebSystemdSocket: OfBool(false), + WebConfigFile: OfString("testdata/web_config_headers.good.yml"), + } + ListenAndServe(server, &flags, testlogger) close(done) }() diff --git a/web/kingpinflag/flag.go b/web/kingpinflag/flag.go index 8cf66ea7..f4caa98e 100644 --- a/web/kingpinflag/flag.go +++ b/web/kingpinflag/flag.go @@ -13,14 +13,34 @@ package kingpinflag import ( + "runtime" + "gopkg.in/alecthomas/kingpin.v2" + + "github.com/prometheus/exporter-toolkit/web" ) // AddFlags adds the flags used by this package to the Kingpin application. -// To use the default Kingpin application, call AddFlags(kingpin.CommandLine) -func AddFlags(a *kingpin.Application) *string { - return a.Flag( - "web.config.file", - "[EXPERIMENTAL] Path to configuration file that can enable TLS or authentication.", - ).Default("").String() +// To use the default Kingpin application, call +// AddFlags(kingpin.CommandLine, ":portNum") where portNum is the default port. +func AddFlags(a *kingpin.Application, defaultAddress string) *web.FlagConfig { + systemdSocket := func() *bool { b := false; return &b }() // Socket activation only available on Linux + if runtime.GOOS == "linux" { + systemdSocket = kingpin.Flag( + "web.systemd-socket", + "Use systemd socket activation listeners instead of port listeners (Linux only).", + ).Bool() + } + flags := web.FlagConfig{ + WebListenAddresses: a.Flag( + "web.listen-address", + "Addresses on which to expose metrics and web interface. Repeatable for multiple addresses.", + ).Default(defaultAddress).Strings(), + WebSystemdSocket: systemdSocket, + WebConfigFile: a.Flag( + "web.config.file", + "[EXPERIMENTAL] Path to configuration file that can enable TLS or authentication.", + ).Default("").String(), + } + return &flags } diff --git a/web/tls_config.go b/web/tls_config.go index 0736163f..47bbca17 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -23,9 +23,11 @@ import ( "os" "path/filepath" + "github.com/coreos/go-systemd/v22/activation" "github.com/go-kit/log" "github.com/go-kit/log/level" config_util "github.com/prometheus/common/config" + "golang.org/x/sync/errgroup" "gopkg.in/yaml.v2" ) @@ -51,6 +53,12 @@ type TLSConfig struct { PreferServerCipherSuites bool `yaml:"prefer_server_cipher_suites"` } +type FlagConfig struct { + WebListenAddresses *[]string + WebSystemdSocket *bool + WebConfigFile *string +} + // SetDirectory joins any relative file paths with dir. func (t *TLSConfig) SetDirectory(dir string) { t.TLSCertPath = config_util.JoinDir(dir, t.TLSCertPath) @@ -177,22 +185,54 @@ func ConfigToTLSConfig(c *TLSConfig) (*tls.Config, error) { return cfg, nil } -// ListenAndServe starts the server on the given address. Based on the file -// tlsConfigPath, TLS or basic auth could be enabled. -func ListenAndServe(server *http.Server, tlsConfigPath string, logger log.Logger) error { - listener, err := net.Listen("tcp", server.Addr) - if err != nil { - return err +// ServeMultiple starts the server on the given listeners. The FlagConfig is +// also passed on to Serve. +func ServeMultiple(listeners []net.Listener, server *http.Server, flags *FlagConfig, logger log.Logger) error { + errs := new(errgroup.Group) + for _, l := range listeners { + l := l + errs.Go(func() error { + return Serve(l, server, flags, logger) + }) + } + return errs.Wait() +} + +// ListenAndServe starts the server on addresses given in WebListenAddresses in +// the FlagConfig or instead uses systemd socket activated listeners if +// WebSystemdSocket in the FlagConfig is true. The FlagConfig is also passed on +// to ServeMultiple. +func ListenAndServe(server *http.Server, flags *FlagConfig, logger log.Logger) error { + if *flags.WebSystemdSocket { + level.Info(logger).Log("msg", "Listening on systemd activated listeners instead of port listeners.") + listeners, err := activation.Listeners() + if err != nil { + return err + } + if len(listeners) < 1 { + return errors.New("no socket activation file descriptors found") + } + return ServeMultiple(listeners, server, flags, logger) } - defer listener.Close() - return Serve(listener, server, tlsConfigPath, logger) + listeners := make([]net.Listener, 0, len(*flags.WebListenAddresses)) + for _, address := range *flags.WebListenAddresses { + listener, err := net.Listen("tcp", address) + if err != nil { + return err + } + defer listener.Close() + listeners = append(listeners, listener) + } + return ServeMultiple(listeners, server, flags, logger) } -// Server starts the server on the given listener. Based on the file -// tlsConfigPath, TLS or basic auth could be enabled. -func Serve(l net.Listener, server *http.Server, tlsConfigPath string, logger log.Logger) error { +// Server starts the server on the given listener. Based on the file path +// WebConfigFile in the FlagConfig, TLS or basic auth could be enabled. +func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger log.Logger) error { + level.Info(logger).Log("msg", "Listening on", "address", l.Addr().String()) + tlsConfigPath := *flags.WebConfigFile if tlsConfigPath == "" { - level.Info(logger).Log("msg", "TLS is disabled.", "http2", false) + level.Info(logger).Log("msg", "TLS is disabled.", "http2", false, "address", l.Addr().String()) return server.Serve(l) } @@ -225,10 +265,10 @@ func Serve(l net.Listener, server *http.Server, tlsConfigPath string, logger log server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) } // Valid TLS config. - level.Info(logger).Log("msg", "TLS is enabled.", "http2", c.HTTPConfig.HTTP2) + level.Info(logger).Log("msg", "TLS is enabled.", "http2", c.HTTPConfig.HTTP2, "address", l.Addr().String()) case errNoTLSConfig: // No TLS config, back to plain HTTP. - level.Info(logger).Log("msg", "TLS is disabled.", "http2", false) + level.Info(logger).Log("msg", "TLS is disabled.", "http2", false, "address", l.Addr().String()) return server.Serve(l) default: // Invalid TLS config. @@ -356,6 +396,6 @@ func (tv *TLSVersion) MarshalYAML() (interface{}, error) { // tlsConfigPath, TLS or basic auth could be enabled. // // Deprecated: Use ListenAndServe instead. -func Listen(server *http.Server, tlsConfigPath string, logger log.Logger) error { - return ListenAndServe(server, tlsConfigPath, logger) +func Listen(server *http.Server, flags *FlagConfig, logger log.Logger) error { + return ListenAndServe(server, flags, logger) } diff --git a/web/tls_config_test.go b/web/tls_config_test.go index 06c7b20a..68aa88b4 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -31,6 +31,14 @@ import ( "time" ) +// Helpers for literal FlagConfig +func OfBool(i bool) *bool { + return &i +} +func OfString(i string) *string { + return &i +} + var ( port = getPort() testlogger = &testLogger{} @@ -378,7 +386,12 @@ func TestConfigReloading(t *testing.T) { recordConnectionError(errors.New("Panic starting server")) } }() - err := Listen(server, badYAMLPath, testlogger) + flagsBadYAMLPath := FlagConfig{ + WebListenAddresses: &([]string{port}), + WebSystemdSocket: OfBool(false), + WebConfigFile: OfString(badYAMLPath), + } + err := Listen(server, &flagsBadYAMLPath, testlogger) recordConnectionError(err) }() @@ -436,7 +449,6 @@ func (test *TestInputs) Test(t *testing.T) { }() server := &http.Server{ - Addr: port, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) }), @@ -448,7 +460,12 @@ func (test *TestInputs) Test(t *testing.T) { recordConnectionError(errors.New("Panic starting server")) } }() - err := ListenAndServe(server, test.YAMLConfigPath, testlogger) + flags := FlagConfig{ + WebListenAddresses: &([]string{port}), + WebSystemdSocket: OfBool(false), + WebConfigFile: &test.YAMLConfigPath, + } + err := ListenAndServe(server, &flags, testlogger) recordConnectionError(err) }()