Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add systemd socket listener activation #95

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -3,10 +3,12 @@ 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/pkg/errors v0.9.1
github.com/prometheus/common v0.37.0
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.4.0
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -53,6 +53,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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=
Expand All @@ -75,6 +77,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=
Expand Down Expand Up @@ -295,6 +298,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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=
Expand Down
24 changes: 18 additions & 6 deletions web/handler_test.go
Expand Up @@ -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!"))
}),
Expand All @@ -39,7 +38,12 @@ func TestBasicAuthCache(t *testing.T) {
})

go func() {
ListenAndServe(server, "testdata/web_config_users_noTLS.good.yml", testlogger)
flags := FlagStruct{
WebListenAddresses: &([]string{port}),
WebSystemdSocket: OfBool(false),
WebConfigFile: OfString("testdata/web_config_users_noTLS.good.yml"),
}
ListenAndServe(server, &flags, testlogger)
close(done)
}()

Expand Down Expand Up @@ -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!"))
}),
Expand All @@ -103,7 +106,12 @@ func TestBasicAuthWithFakepassword(t *testing.T) {
})

go func() {
ListenAndServe(server, "testdata/web_config_users_noTLS.good.yml", testlogger)
flags := FlagStruct{
WebListenAddresses: &([]string{port}),
WebSystemdSocket: OfBool(false),
WebConfigFile: OfString("testdata/web_config_users_noTLS.good.yml"),
}
ListenAndServe(server, &flags, testlogger)
close(done)
}()

Expand Down Expand Up @@ -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!"))
}),
Expand All @@ -147,7 +154,12 @@ func TestHTTPHeaders(t *testing.T) {
})

go func() {
ListenAndServe(server, "testdata/web_config_headers.good.yml", testlogger)
flags := FlagStruct{
WebListenAddresses: &([]string{port}),
WebSystemdSocket: OfBool(false),
WebConfigFile: OfString("testdata/web_config_headers.good.yml"),
}
ListenAndServe(server, &flags, testlogger)
close(done)
}()

Expand Down
32 changes: 26 additions & 6 deletions web/kingpinflag/flag.go
Expand Up @@ -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.FlagStruct {
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.FlagStruct{
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
}
72 changes: 56 additions & 16 deletions web/tls_config.go
Expand Up @@ -22,10 +22,12 @@ import (
"os"
"path/filepath"

"github.com/coreos/go-systemd/v22/activation"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/pkg/errors"
config_util "github.com/prometheus/common/config"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v2"
)

Expand All @@ -51,6 +53,12 @@ type TLSStruct struct {
PreferServerCipherSuites bool `yaml:"prefer_server_cipher_suites"`
}

type FlagStruct struct {
SuperQ marked this conversation as resolved.
Show resolved Hide resolved
WebListenAddresses *[]string
WebSystemdSocket *bool
WebConfigFile *string
}

// SetDirectory joins any relative file paths with dir.
func (t *TLSStruct) SetDirectory(dir string) {
t.TLSCertPath = config_util.JoinDir(dir, t.TLSCertPath)
Expand Down Expand Up @@ -177,22 +185,54 @@ func ConfigToTLSConfig(c *TLSStruct) (*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 FlagStruct is
// also passed on to Serve.
func ServeMultiple(listeners []net.Listener, server *http.Server, flags *FlagStruct, 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 FlagStruct or instead uses systemd socket activated listeners if
// WebSystemdSocket in the FlagStruct is true. The FlagStruct is also passed on
// to ServeMultiple.
func ListenAndServe(server *http.Server, flags *FlagStruct, 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 FlagStruct, TLS or basic auth could be enabled.
func Serve(l net.Listener, server *http.Server, flags *FlagStruct, 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)
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 *FlagStruct, logger log.Logger) error {
return ListenAndServe(server, flags, logger)
}
19 changes: 16 additions & 3 deletions web/tls_config_test.go
Expand Up @@ -31,6 +31,14 @@ import (
"time"
)

// Helpers for literal FlagStruct
func OfBool(i bool) *bool {
return &i
}
func OfString(i string) *string {
return &i
}

var (
port = getPort()
testlogger = &testLogger{}
Expand Down Expand Up @@ -377,7 +385,8 @@ func TestConfigReloading(t *testing.T) {
recordConnectionError(errors.New("Panic starting server"))
}
}()
err := Listen(server, badYAMLPath, testlogger)
flagsBadYAMLPath := FlagStruct{WebConfigFile: OfString(badYAMLPath)}
err := Listen(server, &flagsBadYAMLPath, testlogger)
recordConnectionError(err)
}()

Expand Down Expand Up @@ -435,7 +444,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!"))
}),
Expand All @@ -447,7 +455,12 @@ func (test *TestInputs) Test(t *testing.T) {
recordConnectionError(errors.New("Panic starting server"))
}
}()
err := ListenAndServe(server, test.YAMLConfigPath, testlogger)
flags := FlagStruct{
WebListenAddresses: &([]string{port}),
WebSystemdSocket: OfBool(false),
WebConfigFile: &test.YAMLConfigPath,
}
err := ListenAndServe(server, &flags, testlogger)
recordConnectionError(err)
}()

Expand Down